From 846792a7720be968689b3f87f506b43a1dde6e9c Mon Sep 17 00:00:00 2001 From: shaan1337 Date: Thu, 16 Nov 2023 11:01:22 +0400 Subject: [PATCH 1/7] Bug fix: If TlsVerifyCert is set, don't verify validity of the server certificate --- src/EventStore.Client/ChannelFactory.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs index 06d2888de..7e9c3f7bc 100644 --- a/src/EventStore.Client/ChannelFactory.cs +++ b/src/EventStore.Client/ChannelFactory.cs @@ -1,6 +1,9 @@ using System; +using System.Net; using EndPoint = System.Net.EndPoint; using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using Grpc.Net.Client; using TChannel = Grpc.Net.Client.GrpcChannel; @@ -32,11 +35,17 @@ HttpMessageHandler CreateHandler() { return settings.CreateHttpMessageHandler.Invoke(); } - return new SocketsHttpHandler { + var handler = new SocketsHttpHandler { KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, EnableMultipleHttp2Connections = true }; + + if (!settings.ConnectivitySettings.TlsVerifyCert) { + handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; + } + + return handler; } } } From 6013a3b05808c3a1acc2d04721183072eeb3108d Mon Sep 17 00:00:00 2001 From: shaan1337 Date: Thu, 16 Nov 2023 14:27:50 +0400 Subject: [PATCH 2/7] Implement client certificate authentication --- src/EventStore.Client/CertificateUtils.cs | 61 +++++++++++++++++++ src/EventStore.Client/ChannelFactory.cs | 5 ++ .../EventStoreClientConnectivitySettings.cs | 8 ++- ...entStoreClientSettings.ConnectionString.cs | 30 ++++++++- src/EventStore.Client/HttpFallback.cs | 10 ++- 5 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 src/EventStore.Client/CertificateUtils.cs diff --git a/src/EventStore.Client/CertificateUtils.cs b/src/EventStore.Client/CertificateUtils.cs new file mode 100644 index 000000000..504c834e7 --- /dev/null +++ b/src/EventStore.Client/CertificateUtils.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace EventStore.Client; + +internal class CertificateUtils { + private static RSA LoadKey(string privateKeyPath) { + string[] allLines = File.ReadAllLines(privateKeyPath); + var header = allLines[0].Replace("-", ""); + var privateKey = Convert.FromBase64String(string.Join(string.Empty, allLines.Skip(1).SkipLast(1))); + + var rsa = RSA.Create(); + switch (header) { + case "BEGIN PRIVATE KEY": + rsa.ImportPkcs8PrivateKey(new ReadOnlySpan(privateKey), out _); + break; + case "BEGIN RSA PRIVATE KEY": + rsa.ImportRSAPrivateKey(new ReadOnlySpan(privateKey), out _); + break; + default: + rsa.Dispose(); + throw new NotSupportedException($"Unsupported private key file format: {header}"); + } + + return rsa; + } + + private static X509Certificate2 LoadCertificate(string certificatePath) { + return new X509Certificate2(certificatePath); + } + + internal static X509Certificate2 LoadFromFile(string certificatePath, string privateKeyPath) { + X509Certificate2? publicCertificate = null; + RSA? rsa = null; + + try { + try { + publicCertificate = LoadCertificate(certificatePath); + } catch (Exception ex) { + throw new Exception($"Failed to load certificate: {ex.Message}"); + } + + try { + rsa = LoadKey(privateKeyPath); + } catch (Exception ex) { + throw new Exception($"Failed to load private key: {ex.Message}"); + } + + using var publicWithPrivate = publicCertificate.CopyWithPrivateKey(rsa); + var certificate = new X509Certificate2(publicWithPrivate.Export(X509ContentType.Pfx)); + + return certificate; + } finally { + publicCertificate?.Dispose(); + rsa?.Dispose(); + } + } +} diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs index 7e9c3f7bc..1cf86b93b 100644 --- a/src/EventStore.Client/ChannelFactory.cs +++ b/src/EventStore.Client/ChannelFactory.cs @@ -45,6 +45,11 @@ HttpMessageHandler CreateHandler() { handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; } + var clientCertificate = settings.ConnectivitySettings.ClientCertificate; + if (clientCertificate != null && !settings.ConnectivitySettings.Insecure) { + handler.SslOptions.ClientCertificates = new X509Certificate2Collection(clientCertificate); + } + return handler; } } diff --git a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs index d6e83535b..cf6656e87 100644 --- a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs +++ b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Security.Cryptography.X509Certificates; namespace EventStore.Client { /// @@ -106,7 +107,12 @@ public bool Insecure { /// True if certificates will be validated; otherwise false. /// public bool TlsVerifyCert { get; set; } = true; - + + /// + /// Client certificate used for user authentication. + /// + public X509Certificate2? ClientCertificate { get; set; } = null; + /// /// The default . /// diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs index 76b3c9c6f..497b39e6a 100644 --- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs +++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; using Timeout_ = System.Threading.Timeout; namespace EventStore.Client { @@ -36,6 +37,8 @@ private static class ConnectionStringParser { private const string ThrowOnAppendFailure = nameof(ThrowOnAppendFailure); private const string KeepAliveInterval = nameof(KeepAliveInterval); private const string KeepAliveTimeout = nameof(KeepAliveTimeout); + private const string CertPath = nameof(CertPath); + private const string CertKeyPath = nameof(CertKeyPath); private const string UriSchemeDiscover = "esdb+discover"; @@ -56,6 +59,8 @@ private static class ConnectionStringParser { {ThrowOnAppendFailure, typeof(bool)}, {KeepAliveInterval, typeof(int)}, {KeepAliveTimeout, typeof(int)}, + {CertPath, typeof(string)}, + {CertKeyPath, typeof(string)}, }; public static EventStoreClientSettings Parse(string connectionString) { @@ -75,8 +80,7 @@ public static EventStoreClientSettings Parse(string connectionString) { var slashIndex = connectionString.IndexOf(Slash, currentIndex, StringComparison.Ordinal); - var questionMarkIndex = connectionString.IndexOf(QuestionMark, Math.Max(currentIndex, slashIndex), - StringComparison.Ordinal); + var questionMarkIndex = connectionString.IndexOf(QuestionMark, currentIndex, StringComparison.Ordinal); var endIndex = connectionString.Length; //for simpler substring operations: @@ -198,7 +202,22 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us if (typedOptions.TryGetValue(TlsVerifyCert, out var tlsVerifyCert)) { settings.ConnectivitySettings.TlsVerifyCert = (bool)tlsVerifyCert; } - + + var certPathSet = typedOptions.TryGetValue(CertPath, out var certPath); + var certKeyPathSet = typedOptions.TryGetValue(CertKeyPath, out var certKeyPath); + + if (certPathSet ^ certKeyPathSet) + throw new InvalidSettingException($"Invalid certificate settings. {nameof(CertPath)} and {nameof(CertKeyPath)} must both be set"); + + if (certPathSet && certKeyPathSet) { + try { + settings.ConnectivitySettings.ClientCertificate = + CertificateUtils.LoadFromFile((string)certPath!, (string)certKeyPath!); + } catch (Exception ex) { + throw new InvalidSettingException($"Invalid certificate settings. {ex.Message}"); + } + } + settings.CreateHttpMessageHandler = () => { var handler = new SocketsHttpHandler { KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, @@ -210,6 +229,11 @@ private static EventStoreClientSettings CreateSettings(string scheme, (string us handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; } + var clientCertificate = settings.ConnectivitySettings.ClientCertificate; + if (clientCertificate != null && !settings.ConnectivitySettings.Insecure) { + handler.SslOptions.ClientCertificates = new X509Certificate2Collection(clientCertificate); + } + return handler; }; diff --git a/src/EventStore.Client/HttpFallback.cs b/src/EventStore.Client/HttpFallback.cs index 2a734a1c0..a496a1890 100644 --- a/src/EventStore.Client/HttpFallback.cs +++ b/src/EventStore.Client/HttpFallback.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -17,9 +18,14 @@ internal HttpFallback (EventStoreClientSettings settings) { _defaultCredentials = settings.DefaultCredentials; var handler = new HttpClientHandler(); - if (!settings.ConnectivitySettings.Insecure && !settings.ConnectivitySettings.TlsVerifyCert) { + if (!settings.ConnectivitySettings.Insecure) { handler.ClientCertificateOptions = ClientCertificateOption.Manual; - handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; + + if (settings.ConnectivitySettings.ClientCertificate != null) + handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate); + + if (!settings.ConnectivitySettings.TlsVerifyCert) + handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; } _httpClient = new HttpClient(handler); From 5f969ab66614507a6a8c32e8c20f4565ea3ca4a2 Mon Sep 17 00:00:00 2001 From: William Chong Date: Wed, 13 Mar 2024 10:33:31 +0400 Subject: [PATCH 3/7] Allow overriding user credentials - Replace DnsEndpoint with ChannelIdentifier - Allow passing a new input in SharingProviderInput and add a test - Use BouncyCastle for loading certificates in net48 - Allow passing ChannelIdentifier in ChannelFactory --- .../EventStoreCallOptions.cs | 9 +- .../EventStoreOperationsClient.Admin.cs | 10 +- .../EventStoreOperationsClient.Scavenge.cs | 4 +- ...orePersistentSubscriptionsClient.Create.cs | 2 +- ...orePersistentSubscriptionsClient.Delete.cs | 2 +- ...StorePersistentSubscriptionsClient.Info.cs | 6 +- ...StorePersistentSubscriptionsClient.List.cs | 6 +- ...StorePersistentSubscriptionsClient.Read.cs | 2 +- ...sistentSubscriptionsClient.ReplayParked.cs | 4 +- ...entSubscriptionsClient.RestartSubsystem.cs | 3 +- ...orePersistentSubscriptionsClient.Update.cs | 2 +- ...StoreProjectionManagementClient.Control.cs | 8 +- ...tStoreProjectionManagementClient.Create.cs | 6 +- ...ntStoreProjectionManagementClient.State.cs | 4 +- ...tStoreProjectionManagementClient.Update.cs | 2 +- .../EventStoreClient.Append.cs | 27 ++- .../EventStoreClient.Delete.cs | 2 +- .../EventStoreClient.Metadata.cs | 2 +- .../EventStoreClient.Read.cs | 6 +- .../EventStoreClient.Subscriptions.cs | 4 +- .../EventStoreClient.Tombstone.cs | 2 +- .../EventStoreUserManagementClient.cs | 16 +- src/EventStore.Client/CertificateUtils.cs | 225 ++++-------------- src/EventStore.Client/ChannelCache.cs | 39 ++- src/EventStore.Client/ChannelFactory.cs | 42 ++-- src/EventStore.Client/ChannelIdentifier.cs | 9 + src/EventStore.Client/ChannelSelector.cs | 28 +-- .../EventStore.Client.csproj | 1 + src/EventStore.Client/EventStoreClientBase.cs | 80 ++++--- .../EventStoreClientConnectivitySettings.cs | 8 +- ...entStoreClientSettings.ConnectionString.cs | 70 ++---- .../GossipChannelSelector.cs | 37 +-- src/EventStore.Client/GrpcChannelInput.cs | 23 ++ src/EventStore.Client/GrpcGossipClient.cs | 21 +- src/EventStore.Client/HttpFallback.cs | 25 +- src/EventStore.Client/IChannelSelector.cs | 5 +- .../Interceptors/ReportLeaderInterceptor.cs | 15 +- src/EventStore.Client/NodeSelector.cs | 6 +- src/EventStore.Client/SharingProvider.cs | 49 +++- .../SingleNodeChannelSelector.cs | 21 +- src/EventStore.Client/UserCredentials.cs | 20 +- .../ConnectionStringTests.cs | 10 +- .../CustomHttpMessageHandler.cs | 25 ++ .../GossipChannelSelectorTests.cs | 8 +- .../GrpcServerCapabilitiesClientTests.cs | 2 +- .../ReportLeaderInterceptorTests.cs | 14 +- .../NodeSelectorTests.cs | 14 +- .../SharingProviderTests.cs | 42 +++- 48 files changed, 502 insertions(+), 466 deletions(-) create mode 100644 src/EventStore.Client/ChannelIdentifier.cs create mode 100644 src/EventStore.Client/GrpcChannelInput.cs create mode 100644 test/EventStore.Client.Tests/CustomHttpMessageHandler.cs diff --git a/src/EventStore.Client.Common/EventStoreCallOptions.cs b/src/EventStore.Client.Common/EventStoreCallOptions.cs index e6058a170..ad5df8ca9 100644 --- a/src/EventStore.Client.Common/EventStoreCallOptions.cs +++ b/src/EventStore.Client.Common/EventStoreCallOptions.cs @@ -31,15 +31,15 @@ public static CallOptions CreateNonStreaming( Create( settings, deadline ?? settings.DefaultDeadline, - userCredentials, + userCredentials?.ClientCertificate != null ? null : userCredentials, cancellationToken ); static CallOptions Create( EventStoreClientSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken - ) => - new( + ) { + return new( cancellationToken: cancellationToken, deadline: DeadlineAfter(deadline), headers: new() { @@ -64,6 +64,7 @@ static CallOptions Create( } ) ); + } static DateTime? DeadlineAfter(TimeSpan? timeoutAfter) => !timeoutAfter.HasValue @@ -71,4 +72,4 @@ static CallOptions Create( : timeoutAfter.Value == TimeSpan.MaxValue || timeoutAfter.Value == InfiniteTimeSpan ? DateTime.MaxValue : DateTime.UtcNow.Add(timeoutAfter.Value); -} \ No newline at end of file +} diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs b/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs index bfa750145..bad79cde9 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs +++ b/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs @@ -18,7 +18,7 @@ public async Task ShutdownAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).ShutdownAsync(EmptyResult, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -36,7 +36,7 @@ public async Task MergeIndexesAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).MergeIndexesAsync(EmptyResult, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -54,7 +54,7 @@ public async Task ResignNodeAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).ResignNodeAsync(EmptyResult, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -73,7 +73,7 @@ public async Task SetNodePriorityAsync(int nodePriority, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).SetNodePriorityAsync( new SetNodePriorityReq {Priority = nodePriority}, @@ -92,7 +92,7 @@ public async Task RestartPersistentSubscriptions( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).RestartPersistentSubscriptionsAsync( EmptyResult, diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs b/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs index 43dcfc50f..5534323c3 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs +++ b/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs @@ -29,7 +29,7 @@ public async Task StartScavengeAsync( throw new ArgumentOutOfRangeException(nameof(startFromChunk)); } - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).StartScavengeAsync( new StartScavengeReq { @@ -62,7 +62,7 @@ public async Task StopScavengeAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); var result = await new Operations.Operations.OperationsClient( channelInfo.CallInvoker).StopScavengeAsync(new StopScavengeReq { Options = new StopScavengeReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs index 4cb7acac0..fa17e18ea 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs @@ -198,7 +198,7 @@ private async Task CreateInternalAsync(string streamName, string groupName, IEve "The specified consumer strategy is not supported, specify one of the SystemConsumerStrategies"); } - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs index 40ef522ba..0f02f714e 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs @@ -18,7 +18,7 @@ public Task DeleteAsync(string streamName, string groupName, TimeSpan? deadline /// public async Task DeleteToStreamAsync(string streamName, string groupName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs index b1cc4bebf..b26e6b851 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs @@ -12,8 +12,7 @@ partial class EventStorePersistentSubscriptionsClient { /// public async Task GetInfoToAllAsync(string groupName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { var req = new GetInfoReq() { Options = new GetInfoReq.Types.Options{ @@ -34,8 +33,7 @@ public async Task GetInfoToAllAsync(string groupName /// public async Task GetInfoToStreamAsync(string streamName, string groupName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { var req = new GetInfoReq() { Options = new GetInfoReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs index ce588e3f7..748f7489c 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs @@ -15,7 +15,7 @@ partial class EventStorePersistentSubscriptionsClient { public async Task> ListToAllAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { var req = new ListReq() { Options = new ListReq.Types.Options{ @@ -38,7 +38,7 @@ public async Task> ListToAllAsync(TimeSp public async Task> ListToStreamAsync(string streamName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { var req = new ListReq() { Options = new ListReq.Types.Options { @@ -62,7 +62,7 @@ public async Task> ListToStreamAsync(str public async Task> ListAllAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { var req = new ListReq() { Options = new ListReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs index 148ebcf8d..504f84b44 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs @@ -88,7 +88,7 @@ public PersistentSubscriptionResult SubscribeToStream(string streamName, string } return new PersistentSubscriptionResult(streamName, groupName, async ct => { - var channelInfo = await GetChannelInfo(ct).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, ct).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs index 248ed9143..ef42007ba 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs @@ -14,7 +14,7 @@ partial class EventStorePersistentSubscriptionsClient { public async Task ReplayParkedMessagesToAllAsync(string groupName, long? stopAt = null, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { var req = new ReplayParkedReq() { Options = new ReplayParkedReq.Types.Options{ @@ -46,7 +46,7 @@ await ReplayParkedHttpAsync(SystemStreams.AllStream, groupName, stopAt, channelI public async Task ReplayParkedMessagesToStreamAsync(string streamName, string groupName, long? stopAt=null, TimeSpan? deadline=null, UserCredentials? userCredentials=null, CancellationToken cancellationToken=default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { var req = new ReplayParkedReq() { Options = new ReplayParkedReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs index 4d01967d8..8a2211ea2 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs @@ -10,8 +10,7 @@ partial class EventStorePersistentSubscriptionsClient { /// public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsRestartSubsystem) { await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) .RestartSubsystemAsync(new Empty(), EventStoreCallOptions diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs index 1eae92a25..30e65a464 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs @@ -102,7 +102,7 @@ public async Task UpdateToStreamAsync(string streamName, string groupName, Persi $"{nameof(settings.StartFrom)} must be of type '{nameof(Position)}' when subscribing to {SystemStreams.AllStream}"); } - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs index e21da92fc..dc3d18293 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs @@ -15,7 +15,7 @@ public partial class EventStoreProjectionManagementClient { /// public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).EnableAsync(new EnableReq { Options = new EnableReq.Types.Options { @@ -35,7 +35,7 @@ public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCreden /// public async Task ResetAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).ResetAsync(new ResetReq { Options = new ResetReq.Types.Options { @@ -79,7 +79,7 @@ public Task DisableAsync(string name, TimeSpan? deadline = null, UserCredentials /// public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).RestartSubsystemAsync(new Empty(), EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -88,7 +88,7 @@ public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentia private async Task DisableInternalAsync(string name, bool writeCheckpoint, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).DisableAsync(new DisableReq { Options = new DisableReq.Types.Options { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs index 54498cabf..8692922cd 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs @@ -15,7 +15,7 @@ public partial class EventStoreProjectionManagementClient { /// public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { @@ -39,7 +39,7 @@ public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, public async Task CreateContinuousAsync(string name, string query, bool trackEmittedStreams = false, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { @@ -64,7 +64,7 @@ public async Task CreateContinuousAsync(string name, string query, bool trackEmi /// public async Task CreateTransientAsync(string name, string query, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs index 64187fe1f..00285a88e 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs @@ -73,7 +73,7 @@ public async Task GetResultAsync(string name, string? partition = null, private async ValueTask GetResultInternalAsync(string name, string? partition, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).ResultAsync(new ResultReq { Options = new ResultReq.Types.Options { @@ -148,7 +148,7 @@ public async Task GetStateAsync(string name, string? partition = null, private async ValueTask GetStateInternalAsync(string name, string? partition, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).StateAsync(new StateReq { Options = new StateReq.Types.Options { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs index 8f577e92c..34c25d66c 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs @@ -28,7 +28,7 @@ public async Task UpdateAsync(string name, string query, bool? emitEnabled = nul options.NoEmitOptions = new Empty(); } - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).UpdateAsync(new UpdateReq { Options = options diff --git a/src/EventStore.Client.Streams/EventStoreClient.Append.cs b/src/EventStore.Client.Streams/EventStoreClient.Append.cs index 9d0842855..64f370db2 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Append.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Append.cs @@ -1,14 +1,9 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using System.Threading.Channels; using Google.Protobuf; using EventStore.Client.Streams; using Grpc.Core; using Microsoft.Extensions.Logging; -using System.Runtime.CompilerServices; namespace EventStore.Client { public partial class EventStoreClient { @@ -41,13 +36,19 @@ public async Task AppendToStreamAsync( userCredentials == null && await batchAppender.IsUsable().ConfigureAwait(false) ? batchAppender.Append(streamName, expectedRevision, eventData, deadline, cancellationToken) : AppendToStreamInternal( - (await GetChannelInfo(cancellationToken).ConfigureAwait(false)).CallInvoker, + (await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false)).CallInvoker, new AppendReq { Options = new AppendReq.Types.Options { StreamIdentifier = streamName, Revision = expectedRevision } - }, eventData, options, deadline, userCredentials, cancellationToken); + }, + eventData, + options, + deadline, + userCredentials, + cancellationToken + ); return (await task.ConfigureAwait(false)).OptionallyThrowWrongExpectedVersionException(options); } @@ -81,13 +82,19 @@ public async Task AppendToStreamAsync( userCredentials == null && await batchAppender.IsUsable().ConfigureAwait(false) ? batchAppender.Append(streamName, expectedState, eventData, deadline, cancellationToken) : AppendToStreamInternal( - (await GetChannelInfo(cancellationToken).ConfigureAwait(false)).CallInvoker, + (await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false)).CallInvoker, new AppendReq { Options = new AppendReq.Types.Options { StreamIdentifier = streamName } - }.WithAnyStreamRevision(expectedState), eventData, operationOptions, deadline, userCredentials, - cancellationToken); + }.WithAnyStreamRevision(expectedState), + eventData, + operationOptions, + deadline, + userCredentials, + cancellationToken + ); + return (await task.ConfigureAwait(false)).OptionallyThrowWrongExpectedVersionException(operationOptions); } diff --git a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs b/src/EventStore.Client.Streams/EventStoreClient.Delete.cs index dfaac235f..fb3fe085f 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Delete.cs @@ -53,7 +53,7 @@ private async Task DeleteInternal(DeleteReq request, UserCredentials? userCredentials, CancellationToken cancellationToken) { _log.LogDebug("Deleting stream {streamName}.", request.Options.StreamIdentifier); - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Streams.Streams.StreamsClient( channelInfo.CallInvoker).DeleteAsync(request, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); diff --git a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs b/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs index 6581bd94b..d3d279d53 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs @@ -96,7 +96,7 @@ private async Task SetStreamMetadataInternal(StreamMetadata metada UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); return await AppendToStreamInternal(channelInfo.CallInvoker, appendReq, new[] { new EventData(Uuid.NewUuid(), SystemEventTypes.StreamMetadata, JsonSerializer.SerializeToUtf8Bytes(metadata, StreamMetadataJsonSerializerOptions)), diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/EventStore.Client.Streams/EventStoreClient.Read.cs index d79b83bc6..dcb33e0c6 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Read.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Read.cs @@ -31,7 +31,7 @@ public ReadAllStreamResult ReadAllAsync( } return new ReadAllStreamResult(async _ => { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new() { @@ -103,7 +103,7 @@ public ReadAllStreamResult ReadAllAsync( }; return new ReadAllStreamResult(async _ => { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, readReq, Settings, deadline, userCredentials, cancellationToken); } @@ -238,7 +238,7 @@ public ReadStreamResult ReadStreamAsync( } return new ReadStreamResult(async _ => { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new() { diff --git a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs index f82bd5d07..9d7ad8375 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs @@ -44,7 +44,7 @@ public StreamSubscriptionResult SubscribeToAll( SubscriptionFilterOptions? filterOptions = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => new(async _ => { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new ReadReq.Types.Options { @@ -94,7 +94,7 @@ public StreamSubscriptionResult SubscribeToStream( bool resolveLinkTos = false, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => new(async _ => { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new ReadReq.Types.Options { diff --git a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs b/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs index 9fcddfeb4..d83c96e73 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs @@ -51,7 +51,7 @@ private async Task TombstoneInternal(TombstoneReq request, TimeSpa UserCredentials? userCredentials, CancellationToken cancellationToken) { _log.LogDebug("Tombstoning stream {streamName}.", request.Options.StreamIdentifier); - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Streams.Streams.StreamsClient( channelInfo.CallInvoker).TombstoneAsync(request, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs b/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs index 6b86e81b4..73418ad3b 100644 --- a/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs +++ b/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs @@ -45,7 +45,7 @@ public async Task CreateUserAsync(string loginName, string fullName, string[] gr if (fullName == string.Empty) throw new ArgumentOutOfRangeException(nameof(fullName)); if (password == string.Empty) throw new ArgumentOutOfRangeException(nameof(password)); - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { @@ -78,7 +78,7 @@ public async Task GetUserAsync(string loginName, TimeSpan? deadline throw new ArgumentOutOfRangeException(nameof(loginName)); } - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).Details(new DetailsReq { Options = new DetailsReq.Types.Options { @@ -115,7 +115,7 @@ public async Task DeleteUserAsync(string loginName, TimeSpan? deadline = null, throw new ArgumentOutOfRangeException(nameof(loginName)); } - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); var call = new Users.Users.UsersClient( channelInfo.CallInvoker).DeleteAsync(new DeleteReq { Options = new DeleteReq.Types.Options { @@ -145,7 +145,7 @@ public async Task EnableUserAsync(string loginName, TimeSpan? deadline = null, throw new ArgumentOutOfRangeException(nameof(loginName)); } - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).EnableAsync(new EnableReq { Options = new EnableReq.Types.Options { @@ -168,7 +168,7 @@ public async Task DisableUserAsync(string loginName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); var call = new Users.Users.UsersClient( channelInfo.CallInvoker).DisableAsync(new DisableReq { Options = new DisableReq.Types.Options { @@ -188,7 +188,7 @@ public async Task DisableUserAsync(string loginName, TimeSpan? deadline = null, public async IAsyncEnumerable ListAllAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).Details(new DetailsReq(), EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -224,7 +224,7 @@ public async Task ChangePasswordAsync(string loginName, string currentPassword, if (currentPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(currentPassword)); if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).ChangePasswordAsync( new ChangePasswordReq { @@ -257,7 +257,7 @@ public async Task ResetPasswordAsync(string loginName, string newPassword, if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); - var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); var call = new Users.Users.UsersClient( channelInfo.CallInvoker).ResetPasswordAsync( new ResetPasswordReq { diff --git a/src/EventStore.Client/CertificateUtils.cs b/src/EventStore.Client/CertificateUtils.cs index ed5f4c91e..100aa226c 100644 --- a/src/EventStore.Client/CertificateUtils.cs +++ b/src/EventStore.Client/CertificateUtils.cs @@ -4,9 +4,19 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +#if NET48 +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; +#endif + namespace EventStore.Client; -internal class CertificateUtils { +/// +/// Utility class for loading certificates and private keys from files. +/// +public static class CertificateUtils { private static RSA LoadKey(string privateKeyPath) { string[] allLines = File.ReadAllLines(privateKeyPath); var header = allLines[0].Replace("-", ""); @@ -19,7 +29,12 @@ private static RSA LoadKey(string privateKeyPath) { #if NET rsa.ImportPkcs8PrivateKey(new ReadOnlySpan(privateKey), out _); #else - LightweightPkcs8Decoder.DecodeRSAPkcs8(privateKey); + { + var pemReader = new PemReader(new StringReader(string.Join(Environment.NewLine, allLines))); + var keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); + var privateKeyParams = (RsaPrivateCrtKeyParameters)keyPair.Private; + rsa.ImportParameters(DotNetUtilities.ToRSAParameters(privateKeyParams)); + } #endif break; @@ -27,8 +42,20 @@ private static RSA LoadKey(string privateKeyPath) { #if NET rsa.ImportRSAPrivateKey(new ReadOnlySpan(privateKey), out _); #else - - GetRsaParameters(new ReadOnlySpan(privateKey), true); + { + var pemReader = new PemReader(new StringReader(string.Join(Environment.NewLine, allLines))); + object pemObject = pemReader.ReadObject(); + RsaPrivateCrtKeyParameters privateKeyParams; + if (pemObject is RsaPrivateCrtKeyParameters) { + privateKeyParams = (RsaPrivateCrtKeyParameters)pemObject; + } else if (pemObject is AsymmetricCipherKeyPair keyPair) { + privateKeyParams = (RsaPrivateCrtKeyParameters)keyPair.Private; + } else { + throw new NotSupportedException($"Unsupported PEM object type: {pemObject.GetType()}"); + } + + rsa.ImportParameters(DotNetUtilities.ToRSAParameters(privateKeyParams)); + } #endif break; @@ -40,11 +67,18 @@ private static RSA LoadKey(string privateKeyPath) { return rsa; } - private static X509Certificate2 LoadCertificate(string certificatePath) { + internal static X509Certificate2 LoadCertificate(string certificatePath) { return new X509Certificate2(certificatePath); } - internal static X509Certificate2 LoadFromFile(string certificatePath, string privateKeyPath) { + /// + /// + /// + /// + /// + /// + /// + public static X509Certificate2 LoadFromFile(string certificatePath, string privateKeyPath) { X509Certificate2? publicCertificate = null; RSA? rsa = null; @@ -70,183 +104,4 @@ internal static X509Certificate2 LoadFromFile(string certificatePath, string pri rsa?.Dispose(); } } - - // Derived from https://github.com/mysql-net/MySqlConnector/blob/bbdbd782e7434b765154805b1cb61d8daac68112/src/MySqlConnector/Utilities/Utility.cs#L150 - - // Reads a length encoded according to ASN.1 BER rules. - private static bool TryReadAsnLength(ReadOnlySpan data, out int length, out int bytesConsumed) { - var leadByte = data[0]; - if (leadByte < 0x80) { - // Short form. One octet. Bit 8 has value "0" and bits 7-1 give the length. - length = leadByte; - bytesConsumed = 1; - return true; - } - - // Long form. Two to 127 octets. Bit 8 of first octet has value "1" and bits 7-1 give the number of additional length octets. Second and following octets give the length, base 256, most significant digit first. - if (leadByte == 0x81) { - length = data[1]; - bytesConsumed = 2; - return true; - } - - if (leadByte == 0x82) { - length = data[1] * 256 + data[2]; - bytesConsumed = 3; - return true; - } - - // lengths over 2^16 are not currently handled - length = 0; - bytesConsumed = 0; - return false; - } - - private static bool TryReadAsnInteger( - ReadOnlySpan data, out ReadOnlySpan number, out int bytesConsumed - ) { - // integer tag is 2 - if (data is not [0x02, ..]) { - number = default; - bytesConsumed = 0; - return false; - } - - data = data[1..]; - - // tag is followed by the length of the integer - if (!TryReadAsnLength(data, out var length, out var lengthBytesConsumed)) { - number = default; - bytesConsumed = 0; - return false; - } - - // length is followed by the integer bytes, MSB first - number = data.Slice(lengthBytesConsumed, length); - bytesConsumed = lengthBytesConsumed + length + 1; - - // trim leading zero bytes - while (number is [0, _, ..]) - number = number[1..]; - - return true; - } - - private static RSAParameters GetRsaParameters(ReadOnlySpan data, bool isPrivate) { - // read header (30 81 xx, or 30 82 xx xx) - if (data[0] != 0x30) - throw new FormatException($"Expected 0x30 but read 0x{data[0]:X2}"); - - data = data.Slice(1); - - if (!TryReadAsnLength(data, out var length, out var bytesConsumed)) - throw new FormatException("Couldn't read key length"); - - data = data.Slice(bytesConsumed); - - if (!isPrivate) { - // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" - ReadOnlySpan rsaOid = [ - 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 - ]; - - if (!data.Slice(0, rsaOid.Length).SequenceEqual(rsaOid)) - throw new FormatException( - $"Expected RSA OID but read {BitConverter.ToString(data.Slice(0, 15).ToArray())}" - ); - - data = data.Slice(rsaOid.Length); - - // BIT STRING (0x03) followed by length - if (data[0] != 0x03) - throw new FormatException($"Expected 0x03 but read 0x{data[0]:X2}"); - - data = data.Slice(1); - - if (!TryReadAsnLength(data, out length, out bytesConsumed)) - throw new FormatException("Couldn't read length"); - - data = data.Slice(bytesConsumed); - - // skip NULL byte - if (data[0] != 0x00) - throw new FormatException($"Expected 0x00 but read 0x{data[0]:X2}"); - - data = data.Slice(1); - - // skip next header (30 81 xx, or 30 82 xx xx) - if (data[0] != 0x30) - throw new FormatException($"Expected 0x30 but read 0x{data[0]:X2}"); - - data = data.Slice(1); - - if (!TryReadAsnLength(data, out length, out bytesConsumed)) - throw new FormatException("Couldn't read length"); - - data = data.Slice(bytesConsumed); - } else { - if (!TryReadAsnInteger(data, out var zero, out bytesConsumed) || zero.Length != 1 || zero[0] != 0) - throw new FormatException("Couldn't read zero."); - - data = data.Slice(bytesConsumed); - } - - if (!TryReadAsnInteger(data, out var modulus, out bytesConsumed)) - throw new FormatException("Couldn't read modulus"); - - data = data.Slice(bytesConsumed); - - if (!TryReadAsnInteger(data, out var exponent, out bytesConsumed)) - throw new FormatException("Couldn't read exponent"); - - data = data.Slice(bytesConsumed); - - if (!isPrivate) { - return new RSAParameters { - Modulus = modulus.ToArray(), - Exponent = exponent.ToArray(), - }; - } - - if (!TryReadAsnInteger(data, out var d, out bytesConsumed)) - throw new FormatException("Couldn't read D"); - - data = data.Slice(bytesConsumed); - - if (!TryReadAsnInteger(data, out var p, out bytesConsumed)) - throw new FormatException("Couldn't read P"); - - data = data.Slice(bytesConsumed); - - if (!TryReadAsnInteger(data, out var q, out bytesConsumed)) - throw new FormatException("Couldn't read Q"); - - data = data.Slice(bytesConsumed); - - if (!TryReadAsnInteger(data, out var dp, out bytesConsumed)) - throw new FormatException("Couldn't read DP"); - - data = data.Slice(bytesConsumed); - - if (!TryReadAsnInteger(data, out var dq, out bytesConsumed)) - throw new FormatException("Couldn't read DQ"); - - data = data.Slice(bytesConsumed); - - if (!TryReadAsnInteger(data, out var iq, out bytesConsumed)) - throw new FormatException("Couldn't read IQ"); - - data = data.Slice(bytesConsumed); - - return new RSAParameters { - Modulus = modulus.ToArray(), - Exponent = exponent.ToArray(), - D = d.ToArray(), - P = p.ToArray(), - Q = q.ToArray(), - DP = dp.ToArray(), - DQ = dq.ToArray(), - InverseQ = iq.ToArray(), - }; - } } diff --git a/src/EventStore.Client/ChannelCache.cs b/src/EventStore.Client/ChannelCache.cs index a3369e25f..68c5317e9 100644 --- a/src/EventStore.Client/ChannelCache.cs +++ b/src/EventStore.Client/ChannelCache.cs @@ -1,5 +1,4 @@ -using System.Net; -using TChannel = Grpc.Net.Client.GrpcChannel; +using TChannel = Grpc.Net.Client.GrpcChannel; namespace EventStore.Client { // Maintains Channels keyed by DnsEndPoint so the channels can be reused. @@ -10,33 +9,35 @@ internal class ChannelCache : private readonly EventStoreClientSettings _settings; private readonly Random _random; - private readonly Dictionary _channels; + private readonly Dictionary _channels; private readonly object _lock = new(); private bool _disposed; public ChannelCache(EventStoreClientSettings settings) { _settings = settings; _random = new Random(0); - _channels = new Dictionary( + _channels = new Dictionary( DnsEndPointEqualityComparer.Instance); } - public TChannel GetChannelInfo(DnsEndPoint endPoint) { + public TChannel GetChannelInfo(ChannelIdentifier channelIdentifier) { lock (_lock) { ThrowIfDisposed(); - if (!_channels.TryGetValue(endPoint, out var channel)) { + if (!_channels.TryGetValue(channelIdentifier, out var channel)) { channel = ChannelFactory.CreateChannel( - settings: _settings, - endPoint: endPoint); - _channels[endPoint] = channel; + settings: _settings, + channelIdentifier + ); + + _channels[channelIdentifier] = channel; } return channel; } } - public KeyValuePair[] GetRandomOrderSnapshot() { + public KeyValuePair[] GetRandomOrderSnapshot() { lock (_lock) { ThrowIfDisposed(); @@ -47,7 +48,7 @@ public KeyValuePair[] GetRandomOrderSnapshot() { } // Update the cache to contain channels for exactly these endpoints - public void UpdateCache(IEnumerable endPoints) { + public void UpdateCache(IEnumerable endPoints) { lock (_lock) { ThrowIfDisposed(); @@ -61,7 +62,6 @@ public void UpdateCache(IEnumerable endPoints) { foreach (var endPoint in endPointsToDiscard) { if (!_channels.TryGetValue(endPoint, out var channel)) continue; - _channels.Remove(endPoint); channelsToDispose.Add(channel); } @@ -114,14 +114,15 @@ private void ThrowIfDisposed() { } private static async Task DisposeChannelsAsync(IEnumerable channels) { - foreach (var channel in channels) + foreach (var channel in channels) { await channel.DisposeAsync().ConfigureAwait(false); + } } - private class DnsEndPointEqualityComparer : IEqualityComparer { + private class DnsEndPointEqualityComparer : IEqualityComparer { public static readonly DnsEndPointEqualityComparer Instance = new(); - public bool Equals(DnsEndPoint? x, DnsEndPoint? y) { + public bool Equals(ChannelIdentifier? x, ChannelIdentifier? y) { if (ReferenceEquals(x, y)) return true; if (x is null) @@ -131,14 +132,12 @@ public bool Equals(DnsEndPoint? x, DnsEndPoint? y) { if (x.GetType() != y.GetType()) return false; return - string.Equals(x.Host, y.Host, StringComparison.OrdinalIgnoreCase) && - x.Port == y.Port; + x.GetHashCode() == y.GetHashCode(); } - public int GetHashCode(DnsEndPoint obj) { + public int GetHashCode(ChannelIdentifier obj) { unchecked { - return (StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Host) * 397) ^ - obj.Port; + return (obj.DnsEndpoint.GetHashCode() * 397) ^ (obj.UserCredentials?.GetHashCode() ?? 0); } } } diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs index bbbca077c..a34c42d90 100644 --- a/src/EventStore.Client/ChannelFactory.cs +++ b/src/EventStore.Client/ChannelFactory.cs @@ -1,15 +1,13 @@ using System.Net.Http; using Grpc.Net.Client; -using System.Net.Security; -using EndPoint = System.Net.EndPoint; using TChannel = Grpc.Net.Client.GrpcChannel; namespace EventStore.Client { internal static class ChannelFactory { private const int MaxReceiveMessageLength = 17 * 1024 * 1024; - public static TChannel CreateChannel(EventStoreClientSettings settings, EndPoint endPoint) { - var address = endPoint.ToUri(!settings.ConnectivitySettings.Insecure); + public static TChannel CreateChannel(EventStoreClientSettings settings, ChannelIdentifier channelIdentifier) { + var address = channelIdentifier.DnsEndpoint.ToUri(!settings.ConnectivitySettings.Insecure); if (settings.ConnectivitySettings.Insecure) { //this must be switched on before creation of the HttpMessageHandler @@ -25,7 +23,7 @@ public static TChannel CreateChannel(EventStoreClientSettings settings, EndPoint DefaultRequestVersion = new Version(2, 0) }, #else - HttpHandler = CreateHandler(), + HttpHandler = CreateHandler(), #endif LoggerFactory = settings.LoggerFactory, Credentials = settings.ChannelCredentials, @@ -39,20 +37,20 @@ HttpMessageHandler CreateHandler() { return settings.CreateHttpMessageHandler.Invoke(); } - var configureClientCert = settings.ConnectivitySettings is { ClientCertificate: not null, Insecure: false }; + bool configureClientCert = settings.ConnectivitySettings.ClientCertificate != null + || settings.ConnectivitySettings.TlsCaFile != null + || channelIdentifier.UserCredentials?.ClientCertificate != null; + + var certificate = channelIdentifier.UserCredentials?.ClientCertificate + ?? settings.ConnectivitySettings.ClientCertificate + ?? settings.ConnectivitySettings.TlsCaFile; + #if NET var handler = new SocketsHttpHandler { KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, EnableMultipleHttp2Connections = true, }; - - if (configureClientCert) - handler.SslOptions.ClientCertificates = [settings.ConnectivitySettings.ClientCertificate!]; - - if (!settings.ConnectivitySettings.TlsVerifyCert) { - handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; - } #else var handler = new WinHttpHandler { TcpKeepAliveEnabled = true, @@ -60,14 +58,28 @@ HttpMessageHandler CreateHandler() { TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, EnableMultipleHttp2Connections = true }; +#endif + + if (settings.ConnectivitySettings.Insecure) return handler; - if (configureClientCert) - handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate!); +#if NET + if (configureClientCert) { + handler.SslOptions.ClientCertificates = [certificate!]; + } + + if (!settings.ConnectivitySettings.TlsVerifyCert) { + handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; + } +#else + if (configureClientCert) { + handler.ClientCertificates.Add(certificate!); + } if (!settings.ConnectivitySettings.TlsVerifyCert) { handler.ServerCertificateValidationCallback = delegate { return true; }; } #endif + return handler; } } diff --git a/src/EventStore.Client/ChannelIdentifier.cs b/src/EventStore.Client/ChannelIdentifier.cs new file mode 100644 index 000000000..8865c09f5 --- /dev/null +++ b/src/EventStore.Client/ChannelIdentifier.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace EventStore.Client { + internal class ChannelIdentifier(DnsEndPoint dnsEndpoint, UserCredentials? userCredentials = null) { + public DnsEndPoint DnsEndpoint { get; } = dnsEndpoint; + + public UserCredentials? UserCredentials { get; } = userCredentials; + } +} diff --git a/src/EventStore.Client/ChannelSelector.cs b/src/EventStore.Client/ChannelSelector.cs index 354c0a9f5..3ccff926e 100644 --- a/src/EventStore.Client/ChannelSelector.cs +++ b/src/EventStore.Client/ChannelSelector.cs @@ -1,24 +1,20 @@ -using System.Net; -using System.Threading; -using System.Threading.Tasks; using Grpc.Core; namespace EventStore.Client { - internal class ChannelSelector : IChannelSelector { - private readonly IChannelSelector _inner; + internal class ChannelSelector( + EventStoreClientSettings settings, + ChannelCache channelCache + ) + : IChannelSelector { + private readonly IChannelSelector _inner = settings.ConnectivitySettings.IsSingleNode + ? new SingleNodeChannelSelector(settings, channelCache) + : new GossipChannelSelector(settings, channelCache, new GrpcGossipClient(settings)); - public ChannelSelector( - EventStoreClientSettings settings, - ChannelCache channelCache) { - _inner = settings.ConnectivitySettings.IsSingleNode - ? new SingleNodeChannelSelector(settings, channelCache) - : new GossipChannelSelector(settings, channelCache, new GrpcGossipClient(settings)); + public Task SelectChannelAsync(UserCredentials? userCredentials, CancellationToken cancellationToken) { + return _inner.SelectChannelAsync(userCredentials, cancellationToken); } - public Task SelectChannelAsync(CancellationToken cancellationToken) => - _inner.SelectChannelAsync(cancellationToken); - - public ChannelBase SelectChannel(DnsEndPoint endPoint) => - _inner.SelectChannel(endPoint); + public ChannelBase SelectChannel(ChannelIdentifier channelIdentifier) => + _inner.SelectChannel(channelIdentifier); } } diff --git a/src/EventStore.Client/EventStore.Client.csproj b/src/EventStore.Client/EventStore.Client.csproj index ddf36c6af..094ed87fc 100644 --- a/src/EventStore.Client/EventStore.Client.csproj +++ b/src/EventStore.Client/EventStore.Client.csproj @@ -29,6 +29,7 @@ + diff --git a/src/EventStore.Client/EventStoreClientBase.cs b/src/EventStore.Client/EventStoreClientBase.cs index 39f579fcc..897814be7 100644 --- a/src/EventStore.Client/EventStoreClientBase.cs +++ b/src/EventStore.Client/EventStoreClientBase.cs @@ -15,50 +15,57 @@ public abstract class EventStoreClientBase : IAsyncDisposable { private readonly Dictionary> _exceptionMap; - private readonly CancellationTokenSource _cts; - private readonly ChannelCache _channelCache; - private readonly SharingProvider _channelInfoProvider; - private readonly Lazy _httpFallback; - + private readonly CancellationTokenSource _cts; + private readonly ChannelCache _channelCache; + private readonly SharingProvider _channelInfoProvider; + private Lazy _httpFallback; + /// The name of the connection. public string ConnectionName { get; } - + /// The . protected EventStoreClientSettings Settings { get; } /// Constructs a new . - protected EventStoreClientBase(EventStoreClientSettings? settings, - Dictionary> exceptionMap) { - Settings = settings ?? new EventStoreClientSettings(); + protected EventStoreClientBase( + EventStoreClientSettings? settings, + Dictionary> exceptionMap + ) { + Settings = settings ?? new EventStoreClientSettings(); _exceptionMap = exceptionMap; - _cts = new CancellationTokenSource(); + _cts = new CancellationTokenSource(); _channelCache = new(Settings); _httpFallback = new Lazy(() => new HttpFallback(Settings)); - + ConnectionName = Settings.ConnectionName ?? $"ES-{Guid.NewGuid()}"; var channelSelector = new ChannelSelector(Settings, _channelCache); - _channelInfoProvider = new SharingProvider( + _channelInfoProvider = new SharingProvider( factory: (endPoint, onBroken) => GetChannelInfoExpensive(endPoint, onBroken, channelSelector, _cts.Token), factoryRetryDelay: Settings.ConnectivitySettings.DiscoveryInterval, - initialInput: ReconnectionRequired.Rediscover.Instance, - loggerFactory: Settings.LoggerFactory); + previousInput: new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance), + loggerFactory: Settings.LoggerFactory + ); } - + // Select a channel and query its capabilities. This is an expensive call that // we don't want to do often. private async Task GetChannelInfoExpensive( - ReconnectionRequired reconnectionRequired, - Action onReconnectionRequired, + GrpcChannelInput grpcChannelInput, + Action onReconnectionRequired, IChannelSelector channelSelector, CancellationToken cancellationToken) { - - var channel = reconnectionRequired switch { - ReconnectionRequired.Rediscover => await channelSelector.SelectChannelAsync(cancellationToken) + var channel = grpcChannelInput.ReconnectionRequired switch { + ReconnectionRequired.Rediscover => await channelSelector.SelectChannelAsync( + grpcChannelInput.UserCredentials, + cancellationToken + ) .ConfigureAwait(false), - ReconnectionRequired.NewLeader (var endPoint) => channelSelector.SelectChannel(endPoint), - _ => throw new ArgumentException(null, nameof(reconnectionRequired)) + ReconnectionRequired.NewLeader (var endPoint) => channelSelector.SelectChannel( + new ChannelIdentifier(endPoint, grpcChannelInput.UserCredentials) + ), + _ => throw new ArgumentException(null, nameof(grpcChannelInput.ReconnectionRequired)) }; var invoker = channel.CreateCallInvoker() @@ -78,11 +85,26 @@ private async Task GetChannelInfoExpensive( return new(channel, caps, invoker); } - + + /// Gets the current channel info. + protected async ValueTask GetChannelInfo(CancellationToken cancellationToken) { + return await _channelInfoProvider + .GetAsync(new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance)) + .WithCancellation(cancellationToken).ConfigureAwait(false); + } + + /// Gets the current channel info. - protected async ValueTask GetChannelInfo(CancellationToken cancellationToken) => - await _channelInfoProvider.CurrentAsync.WithCancellation(cancellationToken).ConfigureAwait(false); - + protected async ValueTask GetChannelInfo(UserCredentials? userCredentials, CancellationToken cancellationToken) { + var input = userCredentials is null + ? new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance) + : new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance, userCredentials); + + _httpFallback = new Lazy(() => new HttpFallback(Settings, userCredentials)); + + return await _channelInfoProvider + .GetAsync(input).WithCancellation(cancellationToken).ConfigureAwait(false); + } /// /// Only exists so that we can manually trigger rediscovery in the tests @@ -97,7 +119,7 @@ internal Task RediscoverAsync() { /// Returns the result of an HTTP Get request based on the client settings. protected async Task HttpGet(string path, Action onNotFound, ChannelInfo channelInfo, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - + return await _httpFallback.Value .HttpGetAsync(path, channelInfo, deadline, userCredentials, onNotFound, cancellationToken) .ConfigureAwait(false); @@ -106,7 +128,7 @@ protected async Task HttpGet(string path, Action onNotFound, ChannelInfo c /// Executes an HTTP Post request based on the client settings. protected async Task HttpPost(string path, string query, Action onNotFound, ChannelInfo channelInfo, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - + await _httpFallback.Value .HttpPostAsync(path, query, channelInfo, deadline, userCredentials, onNotFound, cancellationToken) .ConfigureAwait(false); @@ -118,7 +140,7 @@ public virtual void Dispose() { _cts.Cancel(); _cts.Dispose(); _channelCache.Dispose(); - + if (_httpFallback.IsValueCreated) { _httpFallback.Value.Dispose(); } diff --git a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs index 9ec720079..92890d2f2 100644 --- a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs +++ b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs @@ -101,10 +101,16 @@ public bool Insecure { /// public bool TlsVerifyCert { get; set; } = true; + /// + /// Path to a certificate file for secure connection. Not required for enabling secure connection. Useful for self-signed certificate + /// that are not installed on the system trust store. + /// + public X509Certificate2? TlsCaFile { get; set; } + /// /// Client certificate used for user authentication. /// - public X509Certificate2? ClientCertificate { get; set; } = null; + public X509Certificate2? ClientCertificate { get; set; } /// /// The default . diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs index 43d551ffd..1657f43e1 100644 --- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs +++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs @@ -1,12 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; -using System.Net.Security; -using System.Security.Authentication; using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; using Timeout_ = System.Threading.Timeout; namespace EventStore.Client { @@ -59,6 +52,7 @@ private static class ConnectionStringParser { { NodePreference, typeof(string) }, { Tls, typeof(bool) }, { TlsVerifyCert, typeof(bool) }, + { TlsCaFile, typeof(string) }, { DefaultDeadline, typeof(int) }, { ThrowOnAppendFailure, typeof(bool) }, { KeepAliveInterval, typeof(int) }, @@ -213,57 +207,35 @@ private static EventStoreClientSettings CreateSettings( settings.ConnectivitySettings.TlsVerifyCert = (bool)tlsVerifyCert; } - var certPathSet = typedOptions.TryGetValue(CertPath, out var certPath); - var certKeyPathSet = typedOptions.TryGetValue(CertKeyPath, out var certKeyPath); - - if (certPathSet ^ certKeyPathSet) - throw new InvalidSettingException($"Invalid certificate settings. {nameof(CertPath)} and {nameof(CertKeyPath)} must both be set"); + if (typedOptions.TryGetValue(TlsCaFile, out var tlsCaFile)) { + var tlsCaFilePath = Path.GetFullPath((string)tlsCaFile); + if (!string.IsNullOrEmpty(tlsCaFilePath) && !File.Exists(tlsCaFilePath)) { + throw new InvalidClientCertificateException($"Failed to load certificate. File was not found."); + } - if (certPathSet && certKeyPathSet) { try { - settings.ConnectivitySettings.ClientCertificate = - CertificateUtils.LoadFromFile((string)certPath!, (string)certKeyPath!); - } catch (Exception ex) { - throw new InvalidSettingException($"Invalid certificate settings. {ex.Message}"); + settings.ConnectivitySettings.TlsCaFile = CertificateUtils.LoadCertificate(tlsCaFilePath); + } catch (CryptographicException) { + throw new InvalidClientCertificateException("Failed to load certificate. Invalid file format."); } } - settings.CreateHttpMessageHandler = CreateDefaultHandler; - - return settings; - - HttpMessageHandler CreateDefaultHandler() { - var configureClientCert = settings.ConnectivitySettings is { ClientCertificate: not null, Insecure: false }; -#if NET - var handler = new SocketsHttpHandler { - KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, - KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, - EnableMultipleHttp2Connections = true, - }; - - if (configureClientCert) - handler.SslOptions.ClientCertificates = [settings.ConnectivitySettings.ClientCertificate!]; + var certPathSet = typedOptions.TryGetValue(CertPath, out var certPath); + var certKeyPathSet = typedOptions.TryGetValue(CertKeyPath, out var certKeyPath); - if (!settings.ConnectivitySettings.TlsVerifyCert) { - handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; - } -#else - var handler = new WinHttpHandler { - TcpKeepAliveEnabled = true, - TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, - TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, - EnableMultipleHttp2Connections = true - }; + if (certPathSet ^ certKeyPathSet) + throw new InvalidSettingException($"Invalid certificate settings. {nameof(CertPath)} and {nameof(CertKeyPath)} must both be set"); - if (configureClientCert) - handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate!); + if (!certPathSet || !certKeyPathSet) return settings; - if (!settings.ConnectivitySettings.TlsVerifyCert) { - handler.ServerCertificateValidationCallback = delegate { return true; }; - } -#endif - return handler; + try { + settings.ConnectivitySettings.ClientCertificate = + CertificateUtils.LoadFromFile((string)certPath!, (string)certKeyPath!); + } catch (Exception ex) { + throw new InvalidSettingException($"Invalid certificate settings. {ex.Message}"); } + + return settings; } private static string ParseScheme(string s) => diff --git a/src/EventStore.Client/GossipChannelSelector.cs b/src/EventStore.Client/GossipChannelSelector.cs index 633ab0d84..d2fb8ca45 100644 --- a/src/EventStore.Client/GossipChannelSelector.cs +++ b/src/EventStore.Client/GossipChannelSelector.cs @@ -1,8 +1,4 @@ -using System; -using System.Linq; using System.Net; -using System.Threading; -using System.Threading.Tasks; using Grpc.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -29,19 +25,18 @@ public GossipChannelSelector( _nodeSelector = new(_settings); } - public ChannelBase SelectChannel(DnsEndPoint endPoint) { - return _channels.GetChannelInfo(endPoint); - } + public ChannelBase SelectChannel(ChannelIdentifier channelIdentifier) => + _channels.GetChannelInfo(channelIdentifier); - public async Task SelectChannelAsync(CancellationToken cancellationToken) { - var endPoint = await DiscoverAsync(cancellationToken).ConfigureAwait(false); + public async Task SelectChannelAsync(UserCredentials? userCredentials, CancellationToken cancellationToken) { + var endPoint = await DiscoverAsync(userCredentials, cancellationToken).ConfigureAwait(false); _log.LogInformation("Successfully discovered candidate at {endPoint}.", endPoint); return _channels.GetChannelInfo(endPoint); } - private async Task DiscoverAsync(CancellationToken cancellationToken) { + private async Task DiscoverAsync(UserCredentials? userCredentials, CancellationToken cancellationToken) { for (var attempt = 1; attempt <= _settings.ConnectivitySettings.MaxDiscoverAttempts; attempt++) { foreach (var kvp in _channels.GetRandomOrderSnapshot()) { var endPointToGetGossip = kvp.Key; @@ -52,16 +47,22 @@ private async Task DiscoverAsync(CancellationToken cancellationToke .GetAsync(channelToGetGossip, cancellationToken) .ConfigureAwait(false); - var selectedEndpoint = _nodeSelector.SelectNode(clusterInfo); + var selectedEndpoint = _nodeSelector.SelectNode(clusterInfo, userCredentials); // Successfully selected an endpoint using this gossip! // We want _channels to contain exactly the nodes in ClusterInfo. // nodes no longer in the cluster can be forgotten. // new nodes are added so we can use them to get gossip. - _channels.UpdateCache(clusterInfo.Members.Select(x => x.EndPoint)); + _channels.UpdateCache( + clusterInfo.Members.Select( + x => new ChannelIdentifier( + x.EndPoint, + userCredentials + ) + ) + ); return selectedEndpoint; - } catch (Exception ex) { _log.Log( GetLogLevelForDiscoveryAttempt(attempt), @@ -73,8 +74,14 @@ private async Task DiscoverAsync(CancellationToken cancellationToke } // couldn't select a node from any _channel. reseed the channels. - _channels.UpdateCache(_settings.ConnectivitySettings.GossipSeeds.Select(endPoint => - endPoint as DnsEndPoint ?? new DnsEndPoint(endPoint.GetHost(), endPoint.GetPort()))); + _channels.UpdateCache( + _settings.ConnectivitySettings.GossipSeeds.Select( + endPoint => new ChannelIdentifier( + new DnsEndPoint(endPoint.GetHost(), endPoint.GetPort()), + userCredentials + ) + ) + ); await Task .Delay(_settings.ConnectivitySettings.DiscoveryInterval, cancellationToken) diff --git a/src/EventStore.Client/GrpcChannelInput.cs b/src/EventStore.Client/GrpcChannelInput.cs new file mode 100644 index 000000000..0bc484a9f --- /dev/null +++ b/src/EventStore.Client/GrpcChannelInput.cs @@ -0,0 +1,23 @@ +namespace EventStore.Client { + internal record GrpcChannelInput( + ReconnectionRequired ReconnectionRequired, + UserCredentials? UserCredentials = null + ) { + public virtual bool Equals(GrpcChannelInput? other) { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return ReconnectionRequired.Equals(other.ReconnectionRequired) && + Equals(UserCredentials, other.UserCredentials); + } + + public override int GetHashCode() { + unchecked { + int hash = 17; + hash = hash * 23 + ReconnectionRequired.GetHashCode(); + hash = hash * 23 + (UserCredentials?.GetHashCode() ?? 0); + return hash; + } + } + } +} diff --git a/src/EventStore.Client/GrpcGossipClient.cs b/src/EventStore.Client/GrpcGossipClient.cs index 655d05b5f..4217a204c 100644 --- a/src/EventStore.Client/GrpcGossipClient.cs +++ b/src/EventStore.Client/GrpcGossipClient.cs @@ -16,15 +16,22 @@ public GrpcGossipClient(EventStoreClientSettings settings) { var client = new Gossip.Gossip.GossipClient(channel); using var call = client.ReadAsync( new Empty(), - EventStoreCallOptions.CreateNonStreaming(_settings, ct)); + EventStoreCallOptions.CreateNonStreaming(_settings, ct) + ); + var result = await call.ResponseAsync.ConfigureAwait(false); - return new(result.Members.Select(x => - new ClusterMessages.MemberInfo( - Uuid.FromDto(x.InstanceId), - (ClusterMessages.VNodeState)x.State, - x.IsAlive, - new DnsEndPoint(x.HttpEndPoint.Address, (int)x.HttpEndPoint.Port))).ToArray()); + return new( + result.Members.Select( + x => + new ClusterMessages.MemberInfo( + Uuid.FromDto(x.InstanceId), + (ClusterMessages.VNodeState)x.State, + x.IsAlive, + new DnsEndPoint(x.HttpEndPoint.Address, (int)x.HttpEndPoint.Port) + ) + ).ToArray() + ); } } } diff --git a/src/EventStore.Client/HttpFallback.cs b/src/EventStore.Client/HttpFallback.cs index 85f981ae6..02e154383 100644 --- a/src/EventStore.Client/HttpFallback.cs +++ b/src/EventStore.Client/HttpFallback.cs @@ -13,19 +13,28 @@ internal class HttpFallback : IDisposable { private readonly UserCredentials? _defaultCredentials; private readonly string _addressScheme; - internal HttpFallback (EventStoreClientSettings settings) { + internal HttpFallback (EventStoreClientSettings settings, UserCredentials? userCredentials = null) { _addressScheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme; _defaultCredentials = settings.DefaultCredentials; - - var handler = new HttpClientHandler(); - if (!settings.ConnectivitySettings.Insecure) { - handler.ClientCertificateOptions = ClientCertificateOption.Manual; - if (settings.ConnectivitySettings.ClientCertificate != null) - handler.ClientCertificates.Add(settings.ConnectivitySettings.ClientCertificate); + var handler = new HttpClientHandler(); + if (!settings.ConnectivitySettings.Insecure) { + handler.ClientCertificateOptions = ClientCertificateOption.Manual; + + bool configureClientCert = settings.ConnectivitySettings.ClientCertificate != null + || settings.ConnectivitySettings.TlsCaFile != null + || userCredentials?.ClientCertificate != null; + + var certificate = userCredentials?.ClientCertificate + ?? settings.ConnectivitySettings.ClientCertificate + ?? settings.ConnectivitySettings.TlsCaFile; + + if (configureClientCert) { + handler.ClientCertificates.Add(certificate!); + } if (!settings.ConnectivitySettings.TlsVerifyCert) - handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; + handler.ServerCertificateCustomValidationCallback = delegate { return true; }; } _httpClient = new HttpClient(handler); diff --git a/src/EventStore.Client/IChannelSelector.cs b/src/EventStore.Client/IChannelSelector.cs index 1765ccebc..d03707fb2 100644 --- a/src/EventStore.Client/IChannelSelector.cs +++ b/src/EventStore.Client/IChannelSelector.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Grpc.Core; @@ -6,9 +7,9 @@ namespace EventStore.Client { internal interface IChannelSelector { // Let the channel selector pick an endpoint. - Task SelectChannelAsync(CancellationToken cancellationToken); + Task SelectChannelAsync(UserCredentials? userCredentials, CancellationToken cancellationToken); // Get a channel for the specified endpoint - ChannelBase SelectChannel(DnsEndPoint endPoint); + ChannelBase SelectChannel(ChannelIdentifier channelIdentifier); } } diff --git a/src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs b/src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs index 6d9327858..92806ce3a 100644 --- a/src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs +++ b/src/EventStore.Client/Interceptors/ReportLeaderInterceptor.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Grpc.Core; using Grpc.Core.Interceptors; @@ -8,12 +5,12 @@ namespace EventStore.Client.Interceptors { // this has become more general than just detecting leader changes. // triggers the action on any rpc exception with StatusCode.Unavailable internal class ReportLeaderInterceptor : Interceptor { - private readonly Action _onReconnectionRequired; + private readonly Action _onReconnectionRequired; private const TaskContinuationOptions ContinuationOptions = TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted; - internal ReportLeaderInterceptor(Action onReconnectionRequired) { + internal ReportLeaderInterceptor(Action onReconnectionRequired) { _onReconnectionRequired = onReconnectionRequired; } @@ -69,13 +66,15 @@ private void OnReconnectionRequired(Task task) { NotLeaderException ex => new ReconnectionRequired.NewLeader(ex.LeaderEndpoint), RpcException { StatusCode: StatusCode.Unavailable - // or StatusCode.Unknown or TODO: use RPC exceptions on server + // or StatusCode.Unknown or TODO: use RPC exceptions on server } => ReconnectionRequired.Rediscover.Instance, _ => ReconnectionRequired.None.Instance }; - if (reconnectionRequired is not ReconnectionRequired.None) - _onReconnectionRequired(reconnectionRequired); + if (reconnectionRequired is not ReconnectionRequired.None) { + // fallback + _onReconnectionRequired(new GrpcChannelInput(reconnectionRequired)); + } } private class StreamWriter : IClientStreamWriter { diff --git a/src/EventStore.Client/NodeSelector.cs b/src/EventStore.Client/NodeSelector.cs index 94c00cc56..ccebb2ee6 100644 --- a/src/EventStore.Client/NodeSelector.cs +++ b/src/EventStore.Client/NodeSelector.cs @@ -36,7 +36,9 @@ public NodeSelector(EventStoreClientSettings settings) { }; } - public DnsEndPoint SelectNode(ClusterMessages.ClusterInfo clusterInfo) { + public ChannelIdentifier SelectNode( + ClusterMessages.ClusterInfo clusterInfo, UserCredentials? userCredentials = null + ) { if (clusterInfo.Members.Length == 0) { throw new Exception("No nodes in cluster info."); } @@ -52,7 +54,7 @@ public DnsEndPoint SelectNode(ClusterMessages.ClusterInfo clusterInfo) { throw new Exception("No nodes are in a connectable state."); } - return node.EndPoint; + return new ChannelIdentifier(node.EndPoint, userCredentials); } } diff --git a/src/EventStore.Client/SharingProvider.cs b/src/EventStore.Client/SharingProvider.cs index 67911a618..6ee224924 100644 --- a/src/EventStore.Client/SharingProvider.cs +++ b/src/EventStore.Client/SharingProvider.cs @@ -31,30 +31,34 @@ public SharingProvider(ILoggerFactory? loggerFactory) { // // This class is thread safe. + internal class SharingProvider : SharingProvider, IDisposable { - private readonly Func, Task> _factory; - private readonly TimeSpan _factoryRetryDelay; - private readonly TInput _initialInput; - private TaskCompletionSource _currentBox; - private bool _disposed; + private readonly Func, Task> + _factory; + + private readonly TimeSpan _factoryRetryDelay; + private TInput _previousInput; + private TaskCompletionSource _currentBox; + private bool _disposed; + private readonly SemaphoreSlim _syncLock = new SemaphoreSlim(1, 1); public SharingProvider( Func, Task> factory, TimeSpan factoryRetryDelay, - TInput initialInput, + TInput previousInput, ILoggerFactory? loggerFactory = null) : base(loggerFactory) { _factory = factory; _factoryRetryDelay = factoryRetryDelay; - _initialInput = initialInput; + _previousInput = previousInput; _currentBox = new(TaskCreationOptions.RunContinuationsAsynchronously); - _ = FillBoxAsync(_currentBox, input: initialInput); + _ = FillBoxAsync(_currentBox, input: previousInput); } public Task CurrentAsync => _currentBox.Task; public void Reset() { - OnBroken(_currentBox, _initialInput); + OnBroken(_currentBox, _previousInput); } // Call this to return a box containing a defective item, or indeed no item at all. @@ -89,7 +93,7 @@ private async Task FillBoxAsync(TaskCompletionSource box, TInput input) box.TrySetException(new ObjectDisposedException(GetType().ToString())); return; } - + try { Log.LogDebug("{type} being produced...", typeof(TOutput).Name); var item = await _factory(input, x => OnBroken(box, x)).ConfigureAwait(false); @@ -100,7 +104,30 @@ private async Task FillBoxAsync(TaskCompletionSource box, TInput input) Log.LogDebug(ex, "{type} production failed. Retrying in {delay}", typeof(TOutput).Name, _factoryRetryDelay); await Task.Delay(_factoryRetryDelay).ConfigureAwait(false); box.TrySetException(ex); - OnBroken(box, _initialInput); + OnBroken(box, _previousInput); + } + } + + public async Task GetAsync(TInput input) { + await _syncLock.WaitAsync().ConfigureAwait(false); + try { + if (Equals(input, _previousInput)) { + return await CurrentAsync.ConfigureAwait(false); + } + + if (_currentBox.Task.IsCompleted && Equals(input, _previousInput)) + return await CurrentAsync.ConfigureAwait(false); + + _previousInput = input; + var newBox = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var originalBox = Interlocked.Exchange(ref _currentBox, newBox); + if (originalBox != newBox) { + _ = FillBoxAsync(newBox, input); + } + + return await CurrentAsync.ConfigureAwait(false); + } finally { + _syncLock.Release(); } } diff --git a/src/EventStore.Client/SingleNodeChannelSelector.cs b/src/EventStore.Client/SingleNodeChannelSelector.cs index 79c3affb0..f297271a4 100644 --- a/src/EventStore.Client/SingleNodeChannelSelector.cs +++ b/src/EventStore.Client/SingleNodeChannelSelector.cs @@ -9,7 +9,7 @@ namespace EventStore.Client { internal class SingleNodeChannelSelector : IChannelSelector { private readonly ILogger _log; private readonly ChannelCache _channelCache; - private readonly DnsEndPoint _endPoint; + private readonly ChannelIdentifier _channelIdentifier; public SingleNodeChannelSelector( EventStoreClientSettings settings, @@ -19,18 +19,21 @@ public SingleNodeChannelSelector( new NullLogger(); _channelCache = channelCache; - - var uri = settings.ConnectivitySettings.ResolvedAddressOrDefault; - _endPoint = new DnsEndPoint(host: uri.Host, port: uri.Port); + + var uri = settings.ConnectivitySettings.ResolvedAddressOrDefault; + + _channelIdentifier = new ChannelIdentifier(new DnsEndPoint(uri.Host, uri.Port)); } - public Task SelectChannelAsync(CancellationToken cancellationToken) => - Task.FromResult(SelectChannel(_endPoint)); + public Task SelectChannelAsync( + UserCredentials? userCredentials, CancellationToken cancellationToken + ) => + Task.FromResult(SelectChannel(_channelIdentifier)); - public ChannelBase SelectChannel(DnsEndPoint endPoint) { - _log.LogInformation("Selected {endPoint}.", endPoint); + public ChannelBase SelectChannel(ChannelIdentifier channelIdentifier) { + _log.LogInformation("Selected {endPoint}.", channelIdentifier); - return _channelCache.GetChannelInfo(endPoint); + return _channelCache.GetChannelInfo(channelIdentifier); } } } diff --git a/src/EventStore.Client/UserCredentials.cs b/src/EventStore.Client/UserCredentials.cs index d944d90d7..ef82e6a4c 100644 --- a/src/EventStore.Client/UserCredentials.cs +++ b/src/EventStore.Client/UserCredentials.cs @@ -1,4 +1,5 @@ using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; using System.Text; using static System.Convert; @@ -11,6 +12,13 @@ public class UserCredentials { // ReSharper disable once InconsistentNaming static readonly UTF8Encoding UTF8NoBom = new UTF8Encoding(false); + /// + /// Constructs a new . + /// + public UserCredentials(X509Certificate2 clientCertificate) { + ClientCertificate = clientCertificate; + } + /// /// Constructs a new . /// @@ -31,7 +39,12 @@ public UserCredentials(string bearerToken) { Authorization = new(Constants.Headers.BearerScheme, bearerToken); } - AuthenticationHeaderValue Authorization { get; } + AuthenticationHeaderValue? Authorization { get; } + + /// + /// The client certificate + /// + public X509Certificate2? ClientCertificate { get; } /// /// The username @@ -44,11 +57,12 @@ public UserCredentials(string bearerToken) { public string? Password { get; } /// - public override string ToString() => Authorization.ToString(); + public override string ToString() => + ClientCertificate != null ? string.Empty : Authorization?.ToString() ?? string.Empty; /// /// Implicitly convert a to a . /// public static implicit operator string(UserCredentials self) => self.ToString(); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/EventStore.Client.Tests/ConnectionStringTests.cs index 3fd6480b1..4b7e6c15f 100644 --- a/test/EventStore.Client.Tests/ConnectionStringTests.cs +++ b/test/EventStore.Client.Tests/ConnectionStringTests.cs @@ -115,9 +115,10 @@ public void valid_connection_string_with_empty_path(string connectionString, Eve [InlineData(false)] [InlineData(true)] public void tls_verify_cert(bool tlsVerifyCert) { - var connectionString = $"esdb://localhost:2113/?tlsVerifyCert={tlsVerifyCert}"; - var result = EventStoreClientSettings.Create(connectionString); - using var handler = result.CreateHttpMessageHandler?.Invoke(); + var connectionString = $"esdb://localhost:2113/?tlsVerifyCert={tlsVerifyCert}"; + var result = EventStoreClientSettings.Create(connectionString); + result.CreateHttpMessageHandler = CustomHttpMessageHandler.CreateDefaultHandler(result); + using var handler = result.CreateHttpMessageHandler?.Invoke(); #if NET var socketsHandler = Assert.IsType(handler); if (!tlsVerifyCert) { @@ -156,7 +157,7 @@ public void tls_verify_cert(bool tlsVerifyCert) { [Theory] [MemberData(nameof(InvalidClientCertificates))] public void connection_string_with_invalid_client_certificate_should_throw(string clientCertificatePath) { - Assert.Throws( + Assert.Throws( () => EventStoreClientSettings.Create( $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&tlsCAFile={clientCertificatePath}" ) @@ -166,6 +167,7 @@ public void connection_string_with_invalid_client_certificate_should_throw(strin [Fact] public void infinite_grpc_timeouts() { var result = EventStoreClientSettings.Create("esdb://localhost:2113?keepAliveInterval=-1&keepAliveTimeout=-1"); + result.CreateHttpMessageHandler = CustomHttpMessageHandler.CreateDefaultHandler(result); Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, result.ConnectivitySettings.KeepAliveInterval); Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, result.ConnectivitySettings.KeepAliveTimeout); diff --git a/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs b/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs new file mode 100644 index 000000000..ce8cacb2b --- /dev/null +++ b/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs @@ -0,0 +1,25 @@ +namespace EventStore.Client.Tests; + +using System.Net.Http; + +internal static class CustomHttpMessageHandler { + internal static Func? CreateDefaultHandler(EventStoreClientSettings settings) { + return () => { +#if NET + var handler = new SocketsHttpHandler { + KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, + KeepAlivePingTimeout = settings.ConnectivitySettings.KeepAliveTimeout, + EnableMultipleHttp2Connections = true, + }; +#else + var handler = new WinHttpHandler { + TcpKeepAliveEnabled = true, + TcpKeepAliveTime = settings.ConnectivitySettings.KeepAliveTimeout, + TcpKeepAliveInterval = settings.ConnectivitySettings.KeepAliveInterval, + EnableMultipleHttp2Connections = true + }; +#endif + return handler; + }; + } +} diff --git a/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs b/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs index 5967f7384..048ddb8bd 100644 --- a/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs +++ b/test/EventStore.Client.Tests/GossipChannelSelectorTests.cs @@ -46,10 +46,10 @@ public async Task ExplicitlySettingEndPointChangesChannels() { ) ); - var channel = await sut.SelectChannelAsync(default); + var channel = await sut.SelectChannelAsync(null, default); Assert.Equal($"{firstSelection.Host}:{firstSelection.Port}", channel.Target); - channel = sut.SelectChannel(secondSelection); + channel = sut.SelectChannel(new ChannelIdentifier(secondSelection)); Assert.Equal($"{secondSelection.Host}:{secondSelection.Port}", channel.Target); } @@ -69,7 +69,7 @@ public async Task ThrowsWhenDiscoveryFails() { var sut = new GossipChannelSelector(settings, channelCache, new BadGossipClient()); - var ex = await Assert.ThrowsAsync(async () => await sut.SelectChannelAsync(default)); + var ex = await Assert.ThrowsAsync(async () => await sut.SelectChannelAsync(null, default)); Assert.Equal(3, ex.MaxDiscoverAttempts); } @@ -93,4 +93,4 @@ CancellationToken cancellationToken ) => throw new NotSupportedException(); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs b/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs index 2b13cf13b..9628b848f 100644 --- a/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs +++ b/test/EventStore.Client.Tests/GrpcServerCapabilitiesClientTests.cs @@ -82,7 +82,7 @@ await sut.GetAsync( new() { CreateHttpMessageHandler = kestrel.CreateHandler }, - new DnsEndPoint("localhost", 80) + new ChannelIdentifier(new DnsEndPoint("localhost", 80)) ) .CreateCallInvoker(), default diff --git a/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs b/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs index 5703c3bd7..f9ff6b67e 100644 --- a/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs +++ b/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs @@ -28,13 +28,13 @@ static IEnumerable GrpcCalls() { [Theory] [MemberData(nameof(ReportsNewLeaderCases))] public async Task ReportsNewLeader(GrpcCall call) { - ReconnectionRequired? actual = default; + GrpcChannelInput? actual = default; var sut = new ReportLeaderInterceptor(result => actual = result); var result = await Assert.ThrowsAsync(() => call(sut, Task.FromException(new NotLeaderException("a.host", 2112)))); - Assert.Equal(new ReconnectionRequired.NewLeader(result.LeaderEndpoint), actual); + Assert.Equal(new ReconnectionRequired.NewLeader(result.LeaderEndpoint), actual?.ReconnectionRequired); } public static IEnumerable ForcesRediscoveryCases() => @@ -45,13 +45,13 @@ from statusCode in ForcesRediscoveryStatusCodes [Theory] [MemberData(nameof(ForcesRediscoveryCases))] public async Task ForcesRediscovery(GrpcCall call, StatusCode statusCode) { - ReconnectionRequired? actual = default; + GrpcChannelInput? actual = default; var sut = new ReportLeaderInterceptor(result => actual = result); await Assert.ThrowsAsync(() => call(sut, Task.FromException(new RpcException(new(statusCode, "oops"))))); - Assert.Equal(ReconnectionRequired.Rediscover.Instance, actual); + Assert.NotNull(actual); } public static IEnumerable DoesNotForceRediscoveryCases() => @@ -64,13 +64,13 @@ from statusCode in Enum.GetValues(typeof(StatusCode)) [Theory] [MemberData(nameof(DoesNotForceRediscoveryCases))] public async Task DoesNotForceRediscovery(GrpcCall call, StatusCode statusCode) { - ReconnectionRequired actual = ReconnectionRequired.None.Instance; + GrpcChannelInput? actual = default; var sut = new ReportLeaderInterceptor(result => actual = result); await Assert.ThrowsAsync(() => call(sut, Task.FromException(new RpcException(new(statusCode, "oops"))))); - Assert.Equal(ReconnectionRequired.None.Instance, actual); + Assert.Equal(ReconnectionRequired.None.Instance, actual?.ReconnectionRequired); } static async Task MakeUnaryCall(Interceptor interceptor, Task? response = null) { @@ -221,4 +221,4 @@ public Task WriteAsync(object message) => ? Task.FromException(_response.Exception!.GetBaseException()) : Task.FromResult(false); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/NodeSelectorTests.cs b/test/EventStore.Client.Tests/NodeSelectorTests.cs index 9815305cd..70d167438 100644 --- a/test/EventStore.Client.Tests/NodeSelectorTests.cs +++ b/test/EventStore.Client.Tests/NodeSelectorTests.cs @@ -54,10 +54,10 @@ internal void InvalidStatesAreNotConsidered( DnsEndPoint allowedNode ) { var sut = new NodeSelector(settings); - var selectedNode = sut.SelectNode(clusterInfo); + var selectedNode = sut.SelectNode(clusterInfo, null); - Assert.Equal(allowedNode.Host, selectedNode.Host); - Assert.Equal(allowedNode.Port, selectedNode.Port); + Assert.Equal(allowedNode.Host, selectedNode.DnsEndpoint.Host); + Assert.Equal(allowedNode.Port, selectedNode.DnsEndpoint.Port); } [Fact] @@ -86,8 +86,8 @@ public void DeadNodesAreNotConsidered() { ) ); - Assert.Equal(allowedNode.Host, selectedNode.Host); - Assert.Equal(allowedNode.Port, selectedNode.Port); + Assert.Equal(allowedNode.Host, selectedNode.DnsEndpoint.Host); + Assert.Equal(allowedNode.Port, selectedNode.DnsEndpoint.Port); } [Theory] @@ -117,6 +117,6 @@ public void CanPrefer(NodePreference nodePreference, string expectedHost) { if (expectedHost == "any") return; - Assert.Equal(expectedHost, selectedNode.Host); + Assert.Equal(expectedHost, selectedNode.DnsEndpoint.Host); } -} \ No newline at end of file +} diff --git a/test/EventStore.Client.Tests/SharingProviderTests.cs b/test/EventStore.Client.Tests/SharingProviderTests.cs index ddb4c9d5f..f67b33cb1 100644 --- a/test/EventStore.Client.Tests/SharingProviderTests.cs +++ b/test/EventStore.Client.Tests/SharingProviderTests.cs @@ -263,4 +263,44 @@ async Task Factory(int input, Action onBroken) { await constructionCompleted.WaitAsync(); Assert.Equal(0, await sut.CurrentAsync); } -} \ No newline at end of file + + [Fact] + public async Task CanGetOrCreateAsyncWithSameAndDifferentInputs() { + var count = 0; + var expensiveCalled = 0; + using var sut = new SharingProvider( + async (x, _) => ExpensiveCall(x), + TimeSpan.FromSeconds(0), + 0 + ); + + // Get the object with input 0 + var result1 = await sut.GetAsync(0); + Assert.Equal(0, result1); + + // Get the object with input 0 again (returns the same object) + var result2 = await sut.GetAsync(0); + Assert.Equal(0, result2); + + // Get the object with input 1 (creates a new object) + var result3 = await sut.GetAsync(1); + Assert.Equal(2, result3); + + // Get the object with input 1 again (returns the same object) + var result4 = await sut.GetAsync(1); + Assert.Equal(2, result4); + + // Get the object with input 2 (creates a new object) + var result5 = await sut.GetAsync(2); + Assert.Equal(4, result5); + + // Expensive should be called 3 times + Assert.Equal(3, expensiveCalled); + return; + + int ExpensiveCall(int x) { + expensiveCalled++; + return x + count++; + } + } +} From 1635132111b39f7f8ae16119899384605bd48bf8 Mon Sep 17 00:00:00 2001 From: YoEight Date: Wed, 14 Feb 2024 17:10:03 -0500 Subject: [PATCH 4/7] Move to Cloudsmith docker registry. --- .github/workflows/base.yml | 10 +++++++++- .github/workflows/ci.yml | 1 + .github/workflows/dispatch.yml | 1 + .github/workflows/lts.yml | 1 + .github/workflows/previous-lts.yml | 1 + .github/workflows/publish.yml | 5 ++++- .../GlobalEnvironment.cs | 2 +- .../docker-compose.cluster.yml | 8 ++++---- .../docker-compose.node.yml | 2 +- test/EventStore.Client.Tests.Common/docker-compose.yml | 8 ++++---- 10 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index b3b6facd7..d3f8f78b4 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -25,10 +25,18 @@ jobs: - shell: bash run: | git fetch --prune --unshallow + + - name: Login to Cloudsmith + uses: docker/login-action@v3 + with: + registry: docker.eventstore.com + username: ${{ secrets.CLOUDSMITH_CICD_USER }} + password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} + - name: Pull EventStore Image shell: bash run: | - docker pull ghcr.io/eventstore/eventstore:${{ inputs.docker-tag }} + docker pull docker.eventstore.com/eventstore-ce/eventstoredb-ce:${{ inputs.docker-tag }} - name: Install dotnet SDKs uses: actions/setup-dotnet@v3 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31add45a8..864d6d8a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,3 +13,4 @@ jobs: uses: ./.github/workflows/base.yml with: docker-tag: ci + secrets: inherit diff --git a/.github/workflows/dispatch.yml b/.github/workflows/dispatch.yml index 927050291..ecb887177 100644 --- a/.github/workflows/dispatch.yml +++ b/.github/workflows/dispatch.yml @@ -13,3 +13,4 @@ jobs: uses: ./.github/workflows/base.yml with: docker-tag: ${{ inputs.version }} + secrets: inherit diff --git a/.github/workflows/lts.yml b/.github/workflows/lts.yml index fd4e45bb1..405fa4e2b 100644 --- a/.github/workflows/lts.yml +++ b/.github/workflows/lts.yml @@ -13,3 +13,4 @@ jobs: uses: ./.github/workflows/base.yml with: docker-tag: lts + secrets: inherit diff --git a/.github/workflows/previous-lts.yml b/.github/workflows/previous-lts.yml index c7d16ac1c..711e375b7 100644 --- a/.github/workflows/previous-lts.yml +++ b/.github/workflows/previous-lts.yml @@ -13,3 +13,4 @@ jobs: uses: ./.github/workflows/base.yml with: docker-tag: previous-lts + secrets: inherit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 239ea739a..aad29b9f4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -46,7 +46,10 @@ jobs: framework: [ net8.0 ] services: esdb: - image: ghcr.io/eventstore/eventstore:lts + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:lts + credentials: + username: ${{ secrets.CLOUDSMITH_CICD_USER }} + password: ${{ secrets.CLOUDSMITH_CICD_TOKEN }} env: EVENTSTORE_INSECURE: true EVENTSTORE_MEM_DB: false diff --git a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs index 57c632154..d7d26dc67 100644 --- a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs @@ -22,7 +22,7 @@ static void EnsureDefaults(IConfiguration configuration) { configuration.EnsureValue("ES_USE_CLUSTER", "false"); configuration.EnsureValue("ES_USE_EXTERNAL_SERVER", "false"); - configuration.EnsureValue("ES_DOCKER_REGISTRY", "ghcr.io/eventstore/eventstore"); + configuration.EnsureValue("ES_DOCKER_REGISTRY", "docker.eventstore.com/eventstore-ce/eventstoredb-ce"); configuration.EnsureValue("ES_DOCKER_TAG", "ci"); configuration.EnsureValue("ES_DOCKER_IMAGE", $"{configuration["ES_DOCKER_REGISTRY"]}:{configuration["ES_DOCKER_TAG"]}"); diff --git a/test/EventStore.Client.Tests.Common/docker-compose.cluster.yml b/test/EventStore.Client.Tests.Common/docker-compose.cluster.yml index 2c92d5162..b13b1e341 100644 --- a/test/EventStore.Client.Tests.Common/docker-compose.cluster.yml +++ b/test/EventStore.Client.Tests.Common/docker-compose.cluster.yml @@ -40,7 +40,7 @@ services: - cert-gen esdb-node1: - image: ghcr.io/eventstore/eventstore:${ES_DOCKER_TAG} + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} container_name: esdb-node1 env_file: - shared.env @@ -69,7 +69,7 @@ services: - cert-gen esdb-node2: - image: ghcr.io/eventstore/eventstore:${ES_DOCKER_TAG} + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} container_name: esdb-node2 env_file: - shared.env @@ -98,7 +98,7 @@ services: - cert-gen esdb-node3: - image: ghcr.io/eventstore/eventstore:${ES_DOCKER_TAG} + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} container_name: esdb-node3 env_file: - shared.env @@ -127,7 +127,7 @@ services: - cert-gen esdb-node4: - image: ghcr.io/eventstore/eventstore:${ES_DOCKER_TAG} + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} container_name: esdb-node4 env_file: - shared.env diff --git a/test/EventStore.Client.Tests.Common/docker-compose.node.yml b/test/EventStore.Client.Tests.Common/docker-compose.node.yml index a6997819a..63238270a 100644 --- a/test/EventStore.Client.Tests.Common/docker-compose.node.yml +++ b/test/EventStore.Client.Tests.Common/docker-compose.node.yml @@ -7,7 +7,7 @@ networks: services: eventstore: - image: ghcr.io/eventstore/eventstore:${ES_DOCKER_TAG} + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} container_name: eventstore environment: - EVENTSTORE_MEM_DB=true diff --git a/test/EventStore.Client.Tests.Common/docker-compose.yml b/test/EventStore.Client.Tests.Common/docker-compose.yml index 610a27445..0f8e43094 100644 --- a/test/EventStore.Client.Tests.Common/docker-compose.yml +++ b/test/EventStore.Client.Tests.Common/docker-compose.yml @@ -40,7 +40,7 @@ services: - cert-gen esdb-node1: - image: ghcr.io/eventstore/eventstore:${ES_DOCKER_TAG} + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} container_name: esdb-node1 env_file: - shared.env @@ -69,7 +69,7 @@ services: - cert-gen esdb-node2: - image: ghcr.io/eventstore/eventstore:${ES_DOCKER_TAG} + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} container_name: esdb-node2 env_file: - shared.env @@ -98,7 +98,7 @@ services: - cert-gen esdb-node3: - image: ghcr.io/eventstore/eventstore:${ES_DOCKER_TAG} + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} container_name: esdb-node3 env_file: - shared.env @@ -127,7 +127,7 @@ services: - cert-gen esdb-node4: - image: ghcr.io/eventstore/eventstore:${ES_DOCKER_TAG} + image: docker.eventstore.com/eventstore-ce/eventstoredb-ce:${ES_DOCKER_TAG} container_name: esdb-node4 env_file: - shared.env From 61c20e083051b6102b25485122bb0ebd11fc4f65 Mon Sep 17 00:00:00 2001 From: William Chong Date: Thu, 14 Mar 2024 09:00:20 +0400 Subject: [PATCH 5/7] Fix broken tests --- .../CustomHttpMessageHandler.cs | 23 +++++++++++++++++++ .../ReportLeaderInterceptorTests.cs | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs b/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs index ce8cacb2b..4ec0ff979 100644 --- a/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs +++ b/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs @@ -5,6 +5,12 @@ namespace EventStore.Client.Tests; internal static class CustomHttpMessageHandler { internal static Func? CreateDefaultHandler(EventStoreClientSettings settings) { return () => { + bool configureClientCert = settings.ConnectivitySettings.ClientCertificate != null + || settings.ConnectivitySettings.TlsCaFile != null; + + var certificate = settings.ConnectivitySettings.ClientCertificate + ?? settings.ConnectivitySettings.TlsCaFile; + #if NET var handler = new SocketsHttpHandler { KeepAlivePingDelay = settings.ConnectivitySettings.KeepAliveInterval, @@ -19,6 +25,23 @@ internal static class CustomHttpMessageHandler { EnableMultipleHttp2Connections = true }; #endif + + if (settings.ConnectivitySettings.Insecure) return handler; + +#if NET + if (configureClientCert) { + handler.SslOptions.ClientCertificates = [certificate!]; + } + + if (!settings.ConnectivitySettings.TlsVerifyCert) { + handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; + } +#else + if (!settings.ConnectivitySettings.TlsVerifyCert) { + handler.ServerCertificateValidationCallback = delegate { return true; }; + } +#endif + return handler; }; } diff --git a/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs b/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs index f9ff6b67e..72f095e17 100644 --- a/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs +++ b/test/EventStore.Client.Tests/Interceptors/ReportLeaderInterceptorTests.cs @@ -70,7 +70,7 @@ public async Task DoesNotForceRediscovery(GrpcCall call, StatusCode statusCode) await Assert.ThrowsAsync(() => call(sut, Task.FromException(new RpcException(new(statusCode, "oops"))))); - Assert.Equal(ReconnectionRequired.None.Instance, actual?.ReconnectionRequired); + Assert.Null(actual); } static async Task MakeUnaryCall(Interceptor interceptor, Task? response = null) { From 13635ac426549387239653b83a09d44a195f0ef7 Mon Sep 17 00:00:00 2001 From: William Chong Date: Tue, 19 Mar 2024 12:24:26 +0400 Subject: [PATCH 6/7] Add tests --- .github/workflows/base.yml | 20 ++- .github/workflows/enterprise.yml | 19 +++ EventStore.Client.sln | 7 + gencert.ps1 | 18 ++- gencert.sh | 10 +- .../EventStoreCallOptions.cs | 2 +- .../EventStoreOperationsClient.Admin.cs | 10 +- .../EventStoreOperationsClient.Scavenge.cs | 4 +- ...orePersistentSubscriptionsClient.Create.cs | 2 +- ...orePersistentSubscriptionsClient.Delete.cs | 2 +- ...StorePersistentSubscriptionsClient.Info.cs | 4 +- ...StorePersistentSubscriptionsClient.List.cs | 6 +- ...StorePersistentSubscriptionsClient.Read.cs | 2 +- ...sistentSubscriptionsClient.ReplayParked.cs | 4 +- ...entSubscriptionsClient.RestartSubsystem.cs | 2 +- ...orePersistentSubscriptionsClient.Update.cs | 2 +- ...StoreProjectionManagementClient.Control.cs | 8 +- ...tStoreProjectionManagementClient.Create.cs | 6 +- ...ntStoreProjectionManagementClient.State.cs | 4 +- ...tStoreProjectionManagementClient.Update.cs | 2 +- .../EventStoreClient.Append.cs | 4 +- .../EventStoreClient.Delete.cs | 2 +- .../EventStoreClient.Metadata.cs | 2 +- .../EventStoreClient.Read.cs | 6 +- .../EventStoreClient.Subscriptions.cs | 4 +- .../EventStoreClient.Tombstone.cs | 2 +- .../EventStoreUserManagementClient.cs | 16 +-- src/EventStore.Client/CertificateUtils.cs | 2 +- src/EventStore.Client/ChannelCache.cs | 2 +- src/EventStore.Client/ChannelFactory.cs | 8 +- src/EventStore.Client/ChannelIdentifier.cs | 5 +- src/EventStore.Client/ChannelSelector.cs | 5 +- src/EventStore.Client/EventStoreClientBase.cs | 30 ++-- .../EventStoreClientConnectivitySettings.cs | 2 +- ...entStoreClientSettings.ConnectionString.cs | 2 +- .../GossipChannelSelector.cs | 13 +- src/EventStore.Client/GrpcChannelInput.cs | 8 +- src/EventStore.Client/HttpFallback.cs | 10 +- src/EventStore.Client/IChannelSelector.cs | 2 +- src/EventStore.Client/NodeSelector.cs | 5 +- .../SingleNodeChannelSelector.cs | 60 ++++---- src/EventStore.Client/UserCertificate.cs | 30 ++++ src/EventStore.Client/UserCredentials.cs | 8 +- .../EventStore.Client.Plugins.Tests.csproj | 9 ++ .../UserCertificateTests.cs | 130 ++++++++++++++++++ .../Append/append_to_stream.cs | 1 - .../Fixtures/EventStoreTestNode.cs | 7 +- .../GlobalEnvironment.cs | 2 +- .../TestCredentials.cs | 15 +- .../CustomHttpMessageHandler.cs | 4 +- 50 files changed, 387 insertions(+), 143 deletions(-) create mode 100644 .github/workflows/enterprise.yml create mode 100644 src/EventStore.Client/UserCertificate.cs create mode 100644 test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj create mode 100644 test/EventStore.Client.Plugins.Tests/UserCertificateTests.cs diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index d3f8f78b4..895b7cd5b 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -6,6 +6,18 @@ on: docker-tag: required: true type: string + docker-registry: + required: false + type: string + default: docker.eventstore.com/eventstore-ce/eventstoredb-ce + build-matrix: + required: false + type: string + default: '["Streams", "PersistentSubscriptions", "Operations", "UserManagement", "ProjectionManagement"]' + test-matrix: + required: false + type: string + default: '["Streams", "PersistentSubscriptions", "Operations", "UserManagement", "ProjectionManagement"]' jobs: test: @@ -15,7 +27,8 @@ jobs: matrix: framework: [ net6.0, net7.0, net8.0 ] os: [ ubuntu-latest ] - test: [ Streams, PersistentSubscriptions, Operations, UserManagement, ProjectionManagement ] + build: ${{fromJson(inputs.build-matrix)}} + test: ${{fromJson(inputs.test-matrix)}} configuration: [ release ] runs-on: ${{ matrix.os }} name: EventStore.Client.${{ matrix.test }}/${{ matrix.os }}/${{ matrix.framework }}/${{ inputs.docker-tag }} @@ -36,7 +49,7 @@ jobs: - name: Pull EventStore Image shell: bash run: | - docker pull docker.eventstore.com/eventstore-ce/eventstoredb-ce:${{ inputs.docker-tag }} + docker pull ${{ inputs.docker-registry }}:${{ inputs.docker-tag }} - name: Install dotnet SDKs uses: actions/setup-dotnet@v3 with: @@ -47,11 +60,12 @@ jobs: - name: Compile shell: bash run: | - dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client.${{ matrix.test }} + dotnet build --configuration ${{ matrix.configuration }} --framework ${{ matrix.framework }} src/EventStore.Client.${{ matrix.build }} - name: Run Tests shell: bash env: ES_DOCKER_TAG: ${{ inputs.docker-tag }} + ES_DOCKER_REGISTRY: ${{ inputs.docker-registry }} run: | sudo ./gencert.sh dotnet test --configuration ${{ matrix.configuration }} --blame \ diff --git a/.github/workflows/enterprise.yml b/.github/workflows/enterprise.yml new file mode 100644 index 000000000..6c5770cc5 --- /dev/null +++ b/.github/workflows/enterprise.yml @@ -0,0 +1,19 @@ +name: Test CI + +on: + pull_request: + push: + branches: + - master + tags: + - v* + +jobs: + test: + uses: ./.github/workflows/base.yml + with: + docker-tag: 24.2.0-jammy + docker-registry: docker.eventstore.com/eventstore-ee/eventstoredb-commercial + build-matrix: '["Streams", "PersistentSubscriptions", "Operations", "UserManagement", "ProjectionManagement"]' + test-matrix: '["Plugins"]' + secrets: inherit diff --git a/EventStore.Client.sln b/EventStore.Client.sln index 51229f72c..cc38f9e87 100644 --- a/EventStore.Client.sln +++ b/EventStore.Client.sln @@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.UserManag EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Tests.Common", "test\EventStore.Client.Tests.Common\EventStore.Client.Tests.Common.csproj", "{E326832D-DE52-4DE4-9E54-C800908B75F3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.Client.Plugins.Tests", "test\EventStore.Client.Plugins.Tests\EventStore.Client.Plugins.Tests.csproj", "{315B38AF-4574-4E25-992B-CA0D24C95884}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -94,6 +96,10 @@ Global {E326832D-DE52-4DE4-9E54-C800908B75F3}.Debug|x64.Build.0 = Debug|Any CPU {E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.ActiveCfg = Release|Any CPU {E326832D-DE52-4DE4-9E54-C800908B75F3}.Release|x64.Build.0 = Release|Any CPU + {315B38AF-4574-4E25-992B-CA0D24C95884}.Debug|x64.ActiveCfg = Debug|Any CPU + {315B38AF-4574-4E25-992B-CA0D24C95884}.Debug|x64.Build.0 = Debug|Any CPU + {315B38AF-4574-4E25-992B-CA0D24C95884}.Release|x64.ActiveCfg = Release|Any CPU + {315B38AF-4574-4E25-992B-CA0D24C95884}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {D3744A86-DD35-4104-AAEE-84B79062C4A2} = {EA59C1CB-16DA-4F68-AF8A-642A969B4CF8} @@ -109,5 +115,6 @@ Global {6CEB731F-72E1-461F-A6B3-54DBF3FD786C} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} {22634CEE-4F7B-4679-A48D-38A2A8580ECA} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} {E326832D-DE52-4DE4-9E54-C800908B75F3} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} + {315B38AF-4574-4E25-992B-CA0D24C95884} = {C51F2C69-45A9-4D0D-A708-4FC319D5D340} EndGlobalSection EndGlobal diff --git a/gencert.ps1 b/gencert.ps1 index 3908f57e8..1101cc13d 100644 --- a/gencert.ps1 +++ b/gencert.ps1 @@ -4,18 +4,22 @@ Write-Host ">> Generating certificate..." New-Item -ItemType Directory -Path .\certs -Force # Set permissions for the directory -icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX" +icacls .\certs /grant:r "$($env:UserName):(OI)(CI)F" # Pull the Docker image -docker pull eventstore/es-gencert-cli:1.0.2 +docker pull ghcr.io/eventstore/es-gencert-cli:1.3 -# Create CA certificate -docker run --rm --volume ${PWD}\certs:/tmp --user (Get-Process -Id $PID).SessionId eventstore/es-gencert-cli:1.0.2 create-ca -out /tmp/ca +docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-ca -out /tmp/ca -# Create node certificate -docker run --rm --volume ${PWD}\certs:/tmp --user (Get-Process -Id $PID).SessionId eventstore/es-gencert-cli:1.0.2 create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost +docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost + +# Create admin user +docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin + +# Create an invalid user +docker run --rm --volume ${PWD}\certs:/tmp ghcr.io/eventstore/es-gencert-cli create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid # Set permissions recursively for the directory -icacls .\certs /grant:r "$($env:UserName):(OI)(CI)RX" +icacls .\certs /grant:r "$($env:UserName):(OI)(CI)F" Import-Certificate -FilePath ".\certs\ca\ca.crt" -CertStoreLocation Cert:\CurrentUser\Root diff --git a/gencert.sh b/gencert.sh index fa640f624..7cd69b56a 100755 --- a/gencert.sh +++ b/gencert.sh @@ -13,11 +13,15 @@ mkdir -p certs chmod 0755 ./certs -docker pull eventstore/es-gencert-cli:1.0.2 +docker pull ghcr.io/eventstore/es-gencert-cli:1.3 -docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) eventstore/es-gencert-cli:1.0.2 create-ca -out /tmp/ca +docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-ca -out /tmp/ca -docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) eventstore/es-gencert-cli:1.0.2 create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost +docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-node -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/node -ip-addresses 127.0.0.1 -dns-names localhost + +docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-user -username admin -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-admin + +docker run --rm --volume $PWD/certs:/tmp --user $(id -u):$(id -g) ghcr.io/eventstore/es-gencert-cli create-user -username invalid -ca-certificate /tmp/ca/ca.crt -ca-key /tmp/ca/ca.key -out /tmp/user-invalid chmod -R 0755 ./certs diff --git a/src/EventStore.Client.Common/EventStoreCallOptions.cs b/src/EventStore.Client.Common/EventStoreCallOptions.cs index ad5df8ca9..9e0bb1386 100644 --- a/src/EventStore.Client.Common/EventStoreCallOptions.cs +++ b/src/EventStore.Client.Common/EventStoreCallOptions.cs @@ -31,7 +31,7 @@ public static CallOptions CreateNonStreaming( Create( settings, deadline ?? settings.DefaultDeadline, - userCredentials?.ClientCertificate != null ? null : userCredentials, + userCredentials, cancellationToken ); diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs b/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs index bad79cde9..bfa750145 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs +++ b/src/EventStore.Client.Operations/EventStoreOperationsClient.Admin.cs @@ -18,7 +18,7 @@ public async Task ShutdownAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).ShutdownAsync(EmptyResult, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -36,7 +36,7 @@ public async Task MergeIndexesAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).MergeIndexesAsync(EmptyResult, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -54,7 +54,7 @@ public async Task ResignNodeAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).ResignNodeAsync(EmptyResult, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -73,7 +73,7 @@ public async Task SetNodePriorityAsync(int nodePriority, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).SetNodePriorityAsync( new SetNodePriorityReq {Priority = nodePriority}, @@ -92,7 +92,7 @@ public async Task RestartPersistentSubscriptions( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).RestartPersistentSubscriptionsAsync( EmptyResult, diff --git a/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs b/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs index 5534323c3..43dcfc50f 100644 --- a/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs +++ b/src/EventStore.Client.Operations/EventStoreOperationsClient.Scavenge.cs @@ -29,7 +29,7 @@ public async Task StartScavengeAsync( throw new ArgumentOutOfRangeException(nameof(startFromChunk)); } - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Operations.Operations.OperationsClient( channelInfo.CallInvoker).StartScavengeAsync( new StartScavengeReq { @@ -62,7 +62,7 @@ public async Task StopScavengeAsync( TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); var result = await new Operations.Operations.OperationsClient( channelInfo.CallInvoker).StopScavengeAsync(new StopScavengeReq { Options = new StopScavengeReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs index fa17e18ea..fc0e93996 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs @@ -198,7 +198,7 @@ private async Task CreateInternalAsync(string streamName, string groupName, IEve "The specified consumer strategy is not supported, specify one of the SystemConsumerStrategies"); } - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs index 0f02f714e..69ee8a400 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs @@ -18,7 +18,7 @@ public Task DeleteAsync(string streamName, string groupName, TimeSpan? deadline /// public async Task DeleteToStreamAsync(string streamName, string groupName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs index b26e6b851..696dd9c5f 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs @@ -12,7 +12,7 @@ partial class EventStorePersistentSubscriptionsClient { /// public async Task GetInfoToAllAsync(string groupName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { var req = new GetInfoReq() { Options = new GetInfoReq.Types.Options{ @@ -33,7 +33,7 @@ public async Task GetInfoToAllAsync(string groupName /// public async Task GetInfoToStreamAsync(string streamName, string groupName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { var req = new GetInfoReq() { Options = new GetInfoReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs index 748f7489c..d80eabc9f 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs @@ -15,7 +15,7 @@ partial class EventStorePersistentSubscriptionsClient { public async Task> ListToAllAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { var req = new ListReq() { Options = new ListReq.Types.Options{ @@ -38,7 +38,7 @@ public async Task> ListToAllAsync(TimeSp public async Task> ListToStreamAsync(string streamName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { var req = new ListReq() { Options = new ListReq.Types.Options { @@ -62,7 +62,7 @@ public async Task> ListToStreamAsync(str public async Task> ListAllAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { var req = new ListReq() { Options = new ListReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs index 504f84b44..cb9614e39 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs @@ -88,7 +88,7 @@ public PersistentSubscriptionResult SubscribeToStream(string streamName, string } return new PersistentSubscriptionResult(streamName, groupName, async ct => { - var channelInfo = await GetChannelInfo(userCredentials, ct).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, ct).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs index ef42007ba..ecdb9142a 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs @@ -14,7 +14,7 @@ partial class EventStorePersistentSubscriptionsClient { public async Task ReplayParkedMessagesToAllAsync(string groupName, long? stopAt = null, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { var req = new ReplayParkedReq() { Options = new ReplayParkedReq.Types.Options{ @@ -46,7 +46,7 @@ await ReplayParkedHttpAsync(SystemStreams.AllStream, groupName, stopAt, channelI public async Task ReplayParkedMessagesToStreamAsync(string streamName, string groupName, long? stopAt=null, TimeSpan? deadline=null, UserCredentials? userCredentials=null, CancellationToken cancellationToken=default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { var req = new ReplayParkedReq() { Options = new ReplayParkedReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs index 8a2211ea2..cf2058bc5 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs @@ -10,7 +10,7 @@ partial class EventStorePersistentSubscriptionsClient { /// public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsRestartSubsystem) { await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) .RestartSubsystemAsync(new Empty(), EventStoreCallOptions diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs index 30e65a464..6cc407962 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs @@ -102,7 +102,7 @@ public async Task UpdateToStreamAsync(string streamName, string groupName, Persi $"{nameof(settings.StartFrom)} must be of type '{nameof(Position)}' when subscribing to {SystemStreams.AllStream}"); } - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs index dc3d18293..6949be63a 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs @@ -15,7 +15,7 @@ public partial class EventStoreProjectionManagementClient { /// public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).EnableAsync(new EnableReq { Options = new EnableReq.Types.Options { @@ -35,7 +35,7 @@ public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCreden /// public async Task ResetAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).ResetAsync(new ResetReq { Options = new ResetReq.Types.Options { @@ -79,7 +79,7 @@ public Task DisableAsync(string name, TimeSpan? deadline = null, UserCredentials /// public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).RestartSubsystemAsync(new Empty(), EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -88,7 +88,7 @@ public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentia private async Task DisableInternalAsync(string name, bool writeCheckpoint, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).DisableAsync(new DisableReq { Options = new DisableReq.Types.Options { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs index 8692922cd..b8a8f6303 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs @@ -15,7 +15,7 @@ public partial class EventStoreProjectionManagementClient { /// public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { @@ -39,7 +39,7 @@ public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, public async Task CreateContinuousAsync(string name, string query, bool trackEmittedStreams = false, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { @@ -64,7 +64,7 @@ public async Task CreateContinuousAsync(string name, string query, bool trackEmi /// public async Task CreateTransientAsync(string name, string query, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs index 00285a88e..0b8031204 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs @@ -73,7 +73,7 @@ public async Task GetResultAsync(string name, string? partition = null, private async ValueTask GetResultInternalAsync(string name, string? partition, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).ResultAsync(new ResultReq { Options = new ResultReq.Types.Options { @@ -148,7 +148,7 @@ public async Task GetStateAsync(string name, string? partition = null, private async ValueTask GetStateInternalAsync(string name, string? partition, TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).StateAsync(new StateReq { Options = new StateReq.Types.Options { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs index 34c25d66c..8b8169c89 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs @@ -28,7 +28,7 @@ public async Task UpdateAsync(string name, string query, bool? emitEnabled = nul options.NoEmitOptions = new Empty(); } - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).UpdateAsync(new UpdateReq { Options = options diff --git a/src/EventStore.Client.Streams/EventStoreClient.Append.cs b/src/EventStore.Client.Streams/EventStoreClient.Append.cs index 64f370db2..c5b6b7203 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Append.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Append.cs @@ -36,7 +36,7 @@ public async Task AppendToStreamAsync( userCredentials == null && await batchAppender.IsUsable().ConfigureAwait(false) ? batchAppender.Append(streamName, expectedRevision, eventData, deadline, cancellationToken) : AppendToStreamInternal( - (await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false)).CallInvoker, + (await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false)).CallInvoker, new AppendReq { Options = new AppendReq.Types.Options { StreamIdentifier = streamName, @@ -82,7 +82,7 @@ public async Task AppendToStreamAsync( userCredentials == null && await batchAppender.IsUsable().ConfigureAwait(false) ? batchAppender.Append(streamName, expectedState, eventData, deadline, cancellationToken) : AppendToStreamInternal( - (await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false)).CallInvoker, + (await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false)).CallInvoker, new AppendReq { Options = new AppendReq.Types.Options { StreamIdentifier = streamName diff --git a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs b/src/EventStore.Client.Streams/EventStoreClient.Delete.cs index fb3fe085f..f71bd2394 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Delete.cs @@ -53,7 +53,7 @@ private async Task DeleteInternal(DeleteReq request, UserCredentials? userCredentials, CancellationToken cancellationToken) { _log.LogDebug("Deleting stream {streamName}.", request.Options.StreamIdentifier); - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Streams.Streams.StreamsClient( channelInfo.CallInvoker).DeleteAsync(request, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); diff --git a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs b/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs index d3d279d53..08ece5e80 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs @@ -96,7 +96,7 @@ private async Task SetStreamMetadataInternal(StreamMetadata metada UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); return await AppendToStreamInternal(channelInfo.CallInvoker, appendReq, new[] { new EventData(Uuid.NewUuid(), SystemEventTypes.StreamMetadata, JsonSerializer.SerializeToUtf8Bytes(metadata, StreamMetadataJsonSerializerOptions)), diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/EventStore.Client.Streams/EventStoreClient.Read.cs index dcb33e0c6..16c06c187 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Read.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Read.cs @@ -31,7 +31,7 @@ public ReadAllStreamResult ReadAllAsync( } return new ReadAllStreamResult(async _ => { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new() { @@ -103,7 +103,7 @@ public ReadAllStreamResult ReadAllAsync( }; return new ReadAllStreamResult(async _ => { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, readReq, Settings, deadline, userCredentials, cancellationToken); } @@ -238,7 +238,7 @@ public ReadStreamResult ReadStreamAsync( } return new ReadStreamResult(async _ => { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new() { diff --git a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs index 9d7ad8375..8d214e4cc 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs @@ -44,7 +44,7 @@ public StreamSubscriptionResult SubscribeToAll( SubscriptionFilterOptions? filterOptions = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => new(async _ => { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new ReadReq.Types.Options { @@ -94,7 +94,7 @@ public StreamSubscriptionResult SubscribeToStream( bool resolveLinkTos = false, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => new(async _ => { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new ReadReq.Types.Options { diff --git a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs b/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs index d83c96e73..d0b7b0b6a 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs @@ -51,7 +51,7 @@ private async Task TombstoneInternal(TombstoneReq request, TimeSpa UserCredentials? userCredentials, CancellationToken cancellationToken) { _log.LogDebug("Tombstoning stream {streamName}.", request.Options.StreamIdentifier); - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); using var call = new Streams.Streams.StreamsClient( channelInfo.CallInvoker).TombstoneAsync(request, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); diff --git a/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs b/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs index 73418ad3b..6b86e81b4 100644 --- a/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs +++ b/src/EventStore.Client.UserManagement/EventStoreUserManagementClient.cs @@ -45,7 +45,7 @@ public async Task CreateUserAsync(string loginName, string fullName, string[] gr if (fullName == string.Empty) throw new ArgumentOutOfRangeException(nameof(fullName)); if (password == string.Empty) throw new ArgumentOutOfRangeException(nameof(password)); - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { @@ -78,7 +78,7 @@ public async Task GetUserAsync(string loginName, TimeSpan? deadline throw new ArgumentOutOfRangeException(nameof(loginName)); } - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).Details(new DetailsReq { Options = new DetailsReq.Types.Options { @@ -115,7 +115,7 @@ public async Task DeleteUserAsync(string loginName, TimeSpan? deadline = null, throw new ArgumentOutOfRangeException(nameof(loginName)); } - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); var call = new Users.Users.UsersClient( channelInfo.CallInvoker).DeleteAsync(new DeleteReq { Options = new DeleteReq.Types.Options { @@ -145,7 +145,7 @@ public async Task EnableUserAsync(string loginName, TimeSpan? deadline = null, throw new ArgumentOutOfRangeException(nameof(loginName)); } - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).EnableAsync(new EnableReq { Options = new EnableReq.Types.Options { @@ -168,7 +168,7 @@ public async Task DisableUserAsync(string loginName, TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); var call = new Users.Users.UsersClient( channelInfo.CallInvoker).DisableAsync(new DisableReq { Options = new DisableReq.Types.Options { @@ -188,7 +188,7 @@ public async Task DisableUserAsync(string loginName, TimeSpan? deadline = null, public async IAsyncEnumerable ListAllAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).Details(new DetailsReq(), EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -224,7 +224,7 @@ public async Task ChangePasswordAsync(string loginName, string currentPassword, if (currentPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(currentPassword)); if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); using var call = new Users.Users.UsersClient( channelInfo.CallInvoker).ChangePasswordAsync( new ChangePasswordReq { @@ -257,7 +257,7 @@ public async Task ResetPasswordAsync(string loginName, string newPassword, if (loginName == string.Empty) throw new ArgumentOutOfRangeException(nameof(loginName)); if (newPassword == string.Empty) throw new ArgumentOutOfRangeException(nameof(newPassword)); - var channelInfo = await GetChannelInfo(userCredentials, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(cancellationToken).ConfigureAwait(false); var call = new Users.Users.UsersClient( channelInfo.CallInvoker).ResetPasswordAsync( new ResetPasswordReq { diff --git a/src/EventStore.Client/CertificateUtils.cs b/src/EventStore.Client/CertificateUtils.cs index 100aa226c..1cb5ba3a0 100644 --- a/src/EventStore.Client/CertificateUtils.cs +++ b/src/EventStore.Client/CertificateUtils.cs @@ -16,7 +16,7 @@ namespace EventStore.Client; /// /// Utility class for loading certificates and private keys from files. /// -public static class CertificateUtils { +static class CertificateUtils { private static RSA LoadKey(string privateKeyPath) { string[] allLines = File.ReadAllLines(privateKeyPath); var header = allLines[0].Replace("-", ""); diff --git a/src/EventStore.Client/ChannelCache.cs b/src/EventStore.Client/ChannelCache.cs index 68c5317e9..c6f7aaa49 100644 --- a/src/EventStore.Client/ChannelCache.cs +++ b/src/EventStore.Client/ChannelCache.cs @@ -137,7 +137,7 @@ public bool Equals(ChannelIdentifier? x, ChannelIdentifier? y) { public int GetHashCode(ChannelIdentifier obj) { unchecked { - return (obj.DnsEndpoint.GetHashCode() * 397) ^ (obj.UserCredentials?.GetHashCode() ?? 0); + return (obj.DnsEndpoint.GetHashCode() * 397) ^ (obj.UserCertificate?.GetHashCode() ?? 0); } } } diff --git a/src/EventStore.Client/ChannelFactory.cs b/src/EventStore.Client/ChannelFactory.cs index a34c42d90..507c59614 100644 --- a/src/EventStore.Client/ChannelFactory.cs +++ b/src/EventStore.Client/ChannelFactory.cs @@ -37,12 +37,12 @@ HttpMessageHandler CreateHandler() { return settings.CreateHttpMessageHandler.Invoke(); } - bool configureClientCert = settings.ConnectivitySettings.ClientCertificate != null + bool configureClientCert = settings.ConnectivitySettings.UserCertificate != null || settings.ConnectivitySettings.TlsCaFile != null - || channelIdentifier.UserCredentials?.ClientCertificate != null; + || channelIdentifier.UserCertificate != null; - var certificate = channelIdentifier.UserCredentials?.ClientCertificate - ?? settings.ConnectivitySettings.ClientCertificate + var certificate = channelIdentifier?.UserCertificate + ?? settings.ConnectivitySettings.UserCertificate ?? settings.ConnectivitySettings.TlsCaFile; #if NET diff --git a/src/EventStore.Client/ChannelIdentifier.cs b/src/EventStore.Client/ChannelIdentifier.cs index 8865c09f5..5ee89af96 100644 --- a/src/EventStore.Client/ChannelIdentifier.cs +++ b/src/EventStore.Client/ChannelIdentifier.cs @@ -1,9 +1,10 @@ using System.Net; +using System.Security.Cryptography.X509Certificates; namespace EventStore.Client { - internal class ChannelIdentifier(DnsEndPoint dnsEndpoint, UserCredentials? userCredentials = null) { + internal class ChannelIdentifier(DnsEndPoint dnsEndpoint, X509Certificate2? userCertificate = null) { public DnsEndPoint DnsEndpoint { get; } = dnsEndpoint; - public UserCredentials? UserCredentials { get; } = userCredentials; + public X509Certificate2? UserCertificate { get; } = userCertificate; } } diff --git a/src/EventStore.Client/ChannelSelector.cs b/src/EventStore.Client/ChannelSelector.cs index 3ccff926e..6f308c9e9 100644 --- a/src/EventStore.Client/ChannelSelector.cs +++ b/src/EventStore.Client/ChannelSelector.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography.X509Certificates; using Grpc.Core; namespace EventStore.Client { @@ -10,8 +11,8 @@ ChannelCache channelCache ? new SingleNodeChannelSelector(settings, channelCache) : new GossipChannelSelector(settings, channelCache, new GrpcGossipClient(settings)); - public Task SelectChannelAsync(UserCredentials? userCredentials, CancellationToken cancellationToken) { - return _inner.SelectChannelAsync(userCredentials, cancellationToken); + public Task SelectChannelAsync(X509Certificate2? userCertificate, CancellationToken cancellationToken) { + return _inner.SelectChannelAsync(userCertificate, cancellationToken); } public ChannelBase SelectChannel(ChannelIdentifier channelIdentifier) => diff --git a/src/EventStore.Client/EventStoreClientBase.cs b/src/EventStore.Client/EventStoreClientBase.cs index 897814be7..61d233730 100644 --- a/src/EventStore.Client/EventStoreClientBase.cs +++ b/src/EventStore.Client/EventStoreClientBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using EventStore.Client.Interceptors; @@ -44,7 +45,12 @@ Dictionary> exceptionMap factory: (endPoint, onBroken) => GetChannelInfoExpensive(endPoint, onBroken, channelSelector, _cts.Token), factoryRetryDelay: Settings.ConnectivitySettings.DiscoveryInterval, - previousInput: new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance), + previousInput: settings?.ConnectivitySettings.UserCertificate != null + ? new GrpcChannelInput( + ReconnectionRequired.Rediscover.Instance, + settings.ConnectivitySettings.UserCertificate + ) + : new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance), loggerFactory: Settings.LoggerFactory ); } @@ -58,12 +64,12 @@ private async Task GetChannelInfoExpensive( CancellationToken cancellationToken) { var channel = grpcChannelInput.ReconnectionRequired switch { ReconnectionRequired.Rediscover => await channelSelector.SelectChannelAsync( - grpcChannelInput.UserCredentials, + grpcChannelInput.UserCertificate, cancellationToken ) .ConfigureAwait(false), ReconnectionRequired.NewLeader (var endPoint) => channelSelector.SelectChannel( - new ChannelIdentifier(endPoint, grpcChannelInput.UserCredentials) + new ChannelIdentifier(endPoint, grpcChannelInput.UserCertificate) ), _ => throw new ArgumentException(null, nameof(grpcChannelInput.ReconnectionRequired)) }; @@ -88,22 +94,22 @@ private async Task GetChannelInfoExpensive( /// Gets the current channel info. protected async ValueTask GetChannelInfo(CancellationToken cancellationToken) { - return await _channelInfoProvider - .GetAsync(new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance)) - .WithCancellation(cancellationToken).ConfigureAwait(false); + return await _channelInfoProvider.CurrentAsync.WithCancellation(cancellationToken).ConfigureAwait(false); } /// Gets the current channel info. - protected async ValueTask GetChannelInfo(UserCredentials? userCredentials, CancellationToken cancellationToken) { - var input = userCredentials is null - ? new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance) - : new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance, userCredentials); + protected async ValueTask GetChannelInfo(X509Certificate2? userCertificate, CancellationToken cancellationToken) { + _httpFallback = new Lazy(() => new HttpFallback(Settings, userCertificate)); - _httpFallback = new Lazy(() => new HttpFallback(Settings, userCredentials)); + if (userCertificate == null) { + return await _channelInfoProvider.CurrentAsync.WithCancellation(cancellationToken) + .ConfigureAwait(false); + } return await _channelInfoProvider - .GetAsync(input).WithCancellation(cancellationToken).ConfigureAwait(false); + .GetAsync(new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance, userCertificate)) + .WithCancellation(cancellationToken).ConfigureAwait(false); } /// diff --git a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs index 92890d2f2..653e75db5 100644 --- a/src/EventStore.Client/EventStoreClientConnectivitySettings.cs +++ b/src/EventStore.Client/EventStoreClientConnectivitySettings.cs @@ -110,7 +110,7 @@ public bool Insecure { /// /// Client certificate used for user authentication. /// - public X509Certificate2? ClientCertificate { get; set; } + public X509Certificate2? UserCertificate { get; set; } /// /// The default . diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs index 1657f43e1..debe65c8a 100644 --- a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs +++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs @@ -229,7 +229,7 @@ private static EventStoreClientSettings CreateSettings( if (!certPathSet || !certKeyPathSet) return settings; try { - settings.ConnectivitySettings.ClientCertificate = + settings.ConnectivitySettings.UserCertificate = CertificateUtils.LoadFromFile((string)certPath!, (string)certKeyPath!); } catch (Exception ex) { throw new InvalidSettingException($"Invalid certificate settings. {ex.Message}"); diff --git a/src/EventStore.Client/GossipChannelSelector.cs b/src/EventStore.Client/GossipChannelSelector.cs index d2fb8ca45..b04b609c1 100644 --- a/src/EventStore.Client/GossipChannelSelector.cs +++ b/src/EventStore.Client/GossipChannelSelector.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Security.Cryptography.X509Certificates; using Grpc.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -28,15 +29,15 @@ public GossipChannelSelector( public ChannelBase SelectChannel(ChannelIdentifier channelIdentifier) => _channels.GetChannelInfo(channelIdentifier); - public async Task SelectChannelAsync(UserCredentials? userCredentials, CancellationToken cancellationToken) { - var endPoint = await DiscoverAsync(userCredentials, cancellationToken).ConfigureAwait(false); + public async Task SelectChannelAsync(X509Certificate2? userCertificate, CancellationToken cancellationToken) { + var endPoint = await DiscoverAsync(userCertificate, cancellationToken).ConfigureAwait(false); _log.LogInformation("Successfully discovered candidate at {endPoint}.", endPoint); return _channels.GetChannelInfo(endPoint); } - private async Task DiscoverAsync(UserCredentials? userCredentials, CancellationToken cancellationToken) { + private async Task DiscoverAsync(X509Certificate2? userCertificate, CancellationToken cancellationToken) { for (var attempt = 1; attempt <= _settings.ConnectivitySettings.MaxDiscoverAttempts; attempt++) { foreach (var kvp in _channels.GetRandomOrderSnapshot()) { var endPointToGetGossip = kvp.Key; @@ -47,7 +48,7 @@ private async Task DiscoverAsync(UserCredentials? userCredent .GetAsync(channelToGetGossip, cancellationToken) .ConfigureAwait(false); - var selectedEndpoint = _nodeSelector.SelectNode(clusterInfo, userCredentials); + var selectedEndpoint = _nodeSelector.SelectNode(clusterInfo, userCertificate); // Successfully selected an endpoint using this gossip! // We want _channels to contain exactly the nodes in ClusterInfo. @@ -57,7 +58,7 @@ private async Task DiscoverAsync(UserCredentials? userCredent clusterInfo.Members.Select( x => new ChannelIdentifier( x.EndPoint, - userCredentials + userCertificate ) ) ); @@ -78,7 +79,7 @@ private async Task DiscoverAsync(UserCredentials? userCredent _settings.ConnectivitySettings.GossipSeeds.Select( endPoint => new ChannelIdentifier( new DnsEndPoint(endPoint.GetHost(), endPoint.GetPort()), - userCredentials + userCertificate ) ) ); diff --git a/src/EventStore.Client/GrpcChannelInput.cs b/src/EventStore.Client/GrpcChannelInput.cs index 0bc484a9f..55f20863b 100644 --- a/src/EventStore.Client/GrpcChannelInput.cs +++ b/src/EventStore.Client/GrpcChannelInput.cs @@ -1,21 +1,23 @@ +using System.Security.Cryptography.X509Certificates; + namespace EventStore.Client { internal record GrpcChannelInput( ReconnectionRequired ReconnectionRequired, - UserCredentials? UserCredentials = null + X509Certificate2? UserCertificate = null ) { public virtual bool Equals(GrpcChannelInput? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return ReconnectionRequired.Equals(other.ReconnectionRequired) && - Equals(UserCredentials, other.UserCredentials); + Equals(UserCertificate, other.UserCertificate); } public override int GetHashCode() { unchecked { int hash = 17; hash = hash * 23 + ReconnectionRequired.GetHashCode(); - hash = hash * 23 + (UserCredentials?.GetHashCode() ?? 0); + hash = hash * 23 + (UserCertificate?.GetHashCode() ?? 0); return hash; } } diff --git a/src/EventStore.Client/HttpFallback.cs b/src/EventStore.Client/HttpFallback.cs index 02e154383..3b42fc107 100644 --- a/src/EventStore.Client/HttpFallback.cs +++ b/src/EventStore.Client/HttpFallback.cs @@ -13,7 +13,7 @@ internal class HttpFallback : IDisposable { private readonly UserCredentials? _defaultCredentials; private readonly string _addressScheme; - internal HttpFallback (EventStoreClientSettings settings, UserCredentials? userCredentials = null) { + internal HttpFallback (EventStoreClientSettings settings, X509Certificate2? userCertificate = null) { _addressScheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme; _defaultCredentials = settings.DefaultCredentials; @@ -21,12 +21,12 @@ internal HttpFallback (EventStoreClientSettings settings, UserCredentials? userC if (!settings.ConnectivitySettings.Insecure) { handler.ClientCertificateOptions = ClientCertificateOption.Manual; - bool configureClientCert = settings.ConnectivitySettings.ClientCertificate != null + bool configureClientCert = settings.ConnectivitySettings.UserCertificate != null || settings.ConnectivitySettings.TlsCaFile != null - || userCredentials?.ClientCertificate != null; + || userCertificate != null; - var certificate = userCredentials?.ClientCertificate - ?? settings.ConnectivitySettings.ClientCertificate + var certificate = userCertificate + ?? settings.ConnectivitySettings.UserCertificate ?? settings.ConnectivitySettings.TlsCaFile; if (configureClientCert) { diff --git a/src/EventStore.Client/IChannelSelector.cs b/src/EventStore.Client/IChannelSelector.cs index d03707fb2..97539af45 100644 --- a/src/EventStore.Client/IChannelSelector.cs +++ b/src/EventStore.Client/IChannelSelector.cs @@ -7,7 +7,7 @@ namespace EventStore.Client { internal interface IChannelSelector { // Let the channel selector pick an endpoint. - Task SelectChannelAsync(UserCredentials? userCredentials, CancellationToken cancellationToken); + Task SelectChannelAsync(X509Certificate2? userCertificate, CancellationToken cancellationToken); // Get a channel for the specified endpoint ChannelBase SelectChannel(ChannelIdentifier channelIdentifier); diff --git a/src/EventStore.Client/NodeSelector.cs b/src/EventStore.Client/NodeSelector.cs index ccebb2ee6..06cd40c8a 100644 --- a/src/EventStore.Client/NodeSelector.cs +++ b/src/EventStore.Client/NodeSelector.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Security.Cryptography.X509Certificates; namespace EventStore.Client { // Selects a node to connect to from a ClusterInfo, based on the node preference. @@ -37,7 +38,7 @@ public NodeSelector(EventStoreClientSettings settings) { } public ChannelIdentifier SelectNode( - ClusterMessages.ClusterInfo clusterInfo, UserCredentials? userCredentials = null + ClusterMessages.ClusterInfo clusterInfo, X509Certificate2? userCertificate = null ) { if (clusterInfo.Members.Length == 0) { throw new Exception("No nodes in cluster info."); @@ -54,7 +55,7 @@ public ChannelIdentifier SelectNode( throw new Exception("No nodes are in a connectable state."); } - return new ChannelIdentifier(node.EndPoint, userCredentials); + return new ChannelIdentifier(node.EndPoint, userCertificate); } } diff --git a/src/EventStore.Client/SingleNodeChannelSelector.cs b/src/EventStore.Client/SingleNodeChannelSelector.cs index f297271a4..4ad8e6752 100644 --- a/src/EventStore.Client/SingleNodeChannelSelector.cs +++ b/src/EventStore.Client/SingleNodeChannelSelector.cs @@ -1,39 +1,37 @@ using System.Net; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Grpc.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -namespace EventStore.Client { - internal class SingleNodeChannelSelector : IChannelSelector { - private readonly ILogger _log; - private readonly ChannelCache _channelCache; - private readonly ChannelIdentifier _channelIdentifier; - - public SingleNodeChannelSelector( - EventStoreClientSettings settings, - ChannelCache channelCache) { - - _log = settings.LoggerFactory?.CreateLogger() ?? - new NullLogger(); - - _channelCache = channelCache; - - var uri = settings.ConnectivitySettings.ResolvedAddressOrDefault; - - _channelIdentifier = new ChannelIdentifier(new DnsEndPoint(uri.Host, uri.Port)); - } - - public Task SelectChannelAsync( - UserCredentials? userCredentials, CancellationToken cancellationToken - ) => - Task.FromResult(SelectChannel(_channelIdentifier)); - - public ChannelBase SelectChannel(ChannelIdentifier channelIdentifier) { - _log.LogInformation("Selected {endPoint}.", channelIdentifier); - - return _channelCache.GetChannelInfo(channelIdentifier); - } - } +namespace EventStore.Client +{ + internal class SingleNodeChannelSelector : IChannelSelector + { + private readonly ILogger _log; + private readonly ChannelCache _channelCache; + private readonly EventStoreClientSettings _settings; + + public SingleNodeChannelSelector(EventStoreClientSettings settings, ChannelCache channelCache) + { + _log = settings.LoggerFactory?.CreateLogger() ?? new NullLogger(); + _settings = settings; + _channelCache = channelCache; + } + + public Task SelectChannelAsync(X509Certificate2? userCertificate, CancellationToken cancellationToken) + { + var uri = _settings.ConnectivitySettings.ResolvedAddressOrDefault; + var channelIdentifier = new ChannelIdentifier(new DnsEndPoint(uri.Host, uri.Port), userCertificate); + return Task.FromResult(SelectChannel(channelIdentifier)); + } + + public ChannelBase SelectChannel(ChannelIdentifier channelIdentifier) + { + _log.LogInformation("Selected {endPoint}.", channelIdentifier); + return _channelCache.GetChannelInfo(channelIdentifier); + } + } } diff --git a/src/EventStore.Client/UserCertificate.cs b/src/EventStore.Client/UserCertificate.cs new file mode 100644 index 000000000..d9d39236d --- /dev/null +++ b/src/EventStore.Client/UserCertificate.cs @@ -0,0 +1,30 @@ +using System.Security.Cryptography.X509Certificates; + +namespace EventStore.Client { + /// + /// Represents the user certificates used to authenticate and authorize operations on the EventStoreDB. + /// + public record UserCertificate { + /// + /// The user certificate + /// + public X509Certificate2? Certificate { get; } + + /// + /// Constructs a new . + /// + public UserCertificate(X509Certificate2 userCertificate) { + Certificate = userCertificate; + } + + /// + /// Constructs a new . + /// + public UserCertificate(string certificatePath, string privateKeyPath) { + Certificate = CertificateUtils.LoadFromFile( + certificatePath, + privateKeyPath + ); + } + } +} diff --git a/src/EventStore.Client/UserCredentials.cs b/src/EventStore.Client/UserCredentials.cs index ef82e6a4c..5f85cea20 100644 --- a/src/EventStore.Client/UserCredentials.cs +++ b/src/EventStore.Client/UserCredentials.cs @@ -15,8 +15,8 @@ public class UserCredentials { /// /// Constructs a new . /// - public UserCredentials(X509Certificate2 clientCertificate) { - ClientCertificate = clientCertificate; + public UserCredentials(UserCertificate userCertificate) { + UserCertificate = userCertificate.Certificate; } /// @@ -44,7 +44,7 @@ public UserCredentials(string bearerToken) { /// /// The client certificate /// - public X509Certificate2? ClientCertificate { get; } + public X509Certificate2? UserCertificate { get; } /// /// The username @@ -58,7 +58,7 @@ public UserCredentials(string bearerToken) { /// public override string ToString() => - ClientCertificate != null ? string.Empty : Authorization?.ToString() ?? string.Empty; + UserCertificate != null ? string.Empty : Authorization?.ToString() ?? string.Empty; /// /// Implicitly convert a to a . diff --git a/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj b/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj new file mode 100644 index 000000000..89926ac84 --- /dev/null +++ b/test/EventStore.Client.Plugins.Tests/EventStore.Client.Plugins.Tests.csproj @@ -0,0 +1,9 @@ + + + + EventStore.Client.Plugins.Tests + + + + + diff --git a/test/EventStore.Client.Plugins.Tests/UserCertificateTests.cs b/test/EventStore.Client.Plugins.Tests/UserCertificateTests.cs new file mode 100644 index 000000000..ec5e99f27 --- /dev/null +++ b/test/EventStore.Client.Plugins.Tests/UserCertificateTests.cs @@ -0,0 +1,130 @@ +namespace EventStore.Client.Plugins.Tests { + [Trait("Category", "Certificates")] + public class UserCertificateTests(ITestOutputHelper output, EventStoreFixture fixture) + : EventStoreTests(output, fixture) { + [Fact] + public async Task user_credentials_takes_precedence_over_user_certificates() { + var certPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"); + var certKeyPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key"); + + var connectionString = + $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&certPath={certPath}&certKeyPath={certKeyPath}"; + + var stream = Fixture.GetStreamName(); + + var settings = EventStoreClientSettings.Create(connectionString); + + var client = new EventStoreClient(settings); + + await client.AppendToStreamAsync( + stream, + StreamState.Any, + Enumerable.Empty(), + userCredentials: TestCredentials.TestBadUser + ).ShouldThrowAsync(); + } + + [Fact] + public Task does_not_accept_certificates_with_invalid_path() { + var certPath = Path.Combine("invalid.crt"); + var certKeyPath = Path.Combine("invalid.key"); + + var connectionString = + $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&certPath={certPath}&certKeyPath={certKeyPath}"; + + Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + + return Task.CompletedTask; + } + + [Fact] + public async Task append_should_be_successful_with_user_certificates() { + var certPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"); + var certKeyPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key"); + + Assert.True(File.Exists(certPath)); + Assert.True(File.Exists(certKeyPath)); + + var connectionString = + $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&certPath={certPath}&certKeyPath={certKeyPath}"; + + Fixture.Log.Information("connectionString: {connectionString}", connectionString); + + var stream = Fixture.GetStreamName(); + + var settings = EventStoreClientSettings.Create(connectionString); + + var client = new EventStoreClient(settings); + + var result = await client.AppendToStreamAsync( + stream, + StreamState.Any, + Enumerable.Empty() + ); + + Assert.NotNull(result); + } + + [Fact] + public async Task append_with_correct_user_certificate_but_read_with_bad_user_certificate() + { + var connectionString = "esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true"; + + var stream = Fixture.GetStreamName(); + + var settings = EventStoreClientSettings.Create(connectionString); + + var client = new EventStoreClient(settings); + + var appendResult = await client.AppendToStreamAsync( + stream, + StreamState.Any, + Enumerable.Empty(), + userCredentials: TestCredentials.UserAdminCertificate + ); + + Assert.NotNull(appendResult); + + await Fixture.Streams + .ReadStreamAsync( + Direction.Forwards, + stream, + StreamPosition.Start, + userCredentials: TestCredentials.BadUserCertificate + ) + .ShouldThrowAsync(); + } + + [Fact] + public async Task overriding_user_certificate_with_basic_authentication_should_work() { + var certPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"); + var certKeyPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key"); + + var connectionString = + $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&certPath={certPath}&certKeyPath={certKeyPath}"; + + var stream = Fixture.GetStreamName(); + + var settings = EventStoreClientSettings.Create(connectionString); + + var client = new EventStoreClient(settings); + + var appendResult = await client.AppendToStreamAsync( + stream, + StreamState.Any, + Fixture.CreateTestEvents(5), + userCredentials: new UserCredentials("admin", "changeit") + ); + + Assert.NotNull(appendResult); + + var readResult = await Fixture.Streams + .ReadStreamAsync( + Direction.Forwards, + stream, + StreamPosition.Start + ).CountAsync(); + readResult.ShouldBe(5); + } + } +} diff --git a/test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs b/test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs index 989a6b6bf..8bde5952f 100644 --- a/test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs +++ b/test/EventStore.Client.Streams.Tests/Append/append_to_stream.cs @@ -342,7 +342,6 @@ public async Task can_append_multiple_events_at_once() { [Fact] public async Task returns_failure_status_when_conditionally_appending_with_version_mismatch() { var stream = Fixture.GetStreamName(); - var result = await Fixture.Streams.ConditionalAppendToStreamAsync( stream, new StreamRevision(7), diff --git a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs index 11f247691..c875c30c3 100644 --- a/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs +++ b/test/EventStore.Client.Tests.Common/Fixtures/EventStoreTestNode.cs @@ -42,7 +42,12 @@ public static EventStoreFixtureOptions DefaultOptions() { ["EVENTSTORE_DISABLE_LOG_FILE"] = "true", ["EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS"] = $"{NetworkPortProvider.DefaultEsdbPort}" }; - + + if (GlobalEnvironment.DockerImage.Contains("commercial")) { + defaultEnvironment["EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH"] = "/etc/eventstore/certs/ca"; + defaultEnvironment["EventStore__Plugins__UserCertificates__Enabled"] = "true"; + } + // TODO SS: must find a way to enable parallel tests on CI. It works locally. if (port != NetworkPortProvider.DefaultEsdbPort) { if (GlobalEnvironment.Variables.TryGetValue("ES_DOCKER_TAG", out var tag) && tag == "ci") diff --git a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs index d7d26dc67..57c632154 100644 --- a/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs +++ b/test/EventStore.Client.Tests.Common/GlobalEnvironment.cs @@ -22,7 +22,7 @@ static void EnsureDefaults(IConfiguration configuration) { configuration.EnsureValue("ES_USE_CLUSTER", "false"); configuration.EnsureValue("ES_USE_EXTERNAL_SERVER", "false"); - configuration.EnsureValue("ES_DOCKER_REGISTRY", "docker.eventstore.com/eventstore-ce/eventstoredb-ce"); + configuration.EnsureValue("ES_DOCKER_REGISTRY", "ghcr.io/eventstore/eventstore"); configuration.EnsureValue("ES_DOCKER_TAG", "ci"); configuration.EnsureValue("ES_DOCKER_IMAGE", $"{configuration["ES_DOCKER_REGISTRY"]}:{configuration["ES_DOCKER_TAG"]}"); diff --git a/test/EventStore.Client.Tests.Common/TestCredentials.cs b/test/EventStore.Client.Tests.Common/TestCredentials.cs index a489cd13d..1a1d89d75 100644 --- a/test/EventStore.Client.Tests.Common/TestCredentials.cs +++ b/test/EventStore.Client.Tests.Common/TestCredentials.cs @@ -6,4 +6,17 @@ public static class TestCredentials { public static readonly UserCredentials TestUser2 = new("user2", "pa$$2"); public static readonly UserCredentials TestAdmin = new("adm", "admpa$$"); public static readonly UserCredentials TestBadUser = new("badlogin", "badpass"); -} \ No newline at end of file + + public static readonly UserCredentials UserAdminCertificate = new( + new UserCertificate( + Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"), + Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key") + ) + ); + public static readonly UserCredentials BadUserCertificate = new( + new UserCertificate( + Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.crt"), + Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.key") + ) + ); +} diff --git a/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs b/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs index 4ec0ff979..1a17d5d3d 100644 --- a/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs +++ b/test/EventStore.Client.Tests/CustomHttpMessageHandler.cs @@ -5,10 +5,10 @@ namespace EventStore.Client.Tests; internal static class CustomHttpMessageHandler { internal static Func? CreateDefaultHandler(EventStoreClientSettings settings) { return () => { - bool configureClientCert = settings.ConnectivitySettings.ClientCertificate != null + bool configureClientCert = settings.ConnectivitySettings.UserCertificate != null || settings.ConnectivitySettings.TlsCaFile != null; - var certificate = settings.ConnectivitySettings.ClientCertificate + var certificate = settings.ConnectivitySettings.UserCertificate ?? settings.ConnectivitySettings.TlsCaFile; #if NET From a6ad8b1ab651d4072aa853d140b58f9df9f9148e Mon Sep 17 00:00:00 2001 From: William Chong Date: Thu, 21 Mar 2024 16:20:11 +0400 Subject: [PATCH 7/7] Add user certificate field * Remove user cert argument http fallback --- .github/workflows/{enterprise.yml => ee.yml} | 3 +- ...orePersistentSubscriptionsClient.Create.cs | 88 +++++++++--- ...orePersistentSubscriptionsClient.Delete.cs | 35 +++-- ...StorePersistentSubscriptionsClient.Info.cs | 22 ++- ...StorePersistentSubscriptionsClient.List.cs | 33 +++-- ...StorePersistentSubscriptionsClient.Read.cs | 76 +++++++---- ...sistentSubscriptionsClient.ReplayParked.cs | 22 +-- ...entSubscriptionsClient.RestartSubsystem.cs | 9 +- ...orePersistentSubscriptionsClient.Update.cs | 46 +++++-- ...StoreProjectionManagementClient.Control.cs | 58 +++++--- ...tStoreProjectionManagementClient.Create.cs | 35 +++-- ...ntStoreProjectionManagementClient.State.cs | 92 ++++++++++--- ...tStoreProjectionManagementClient.Update.cs | 12 +- .../EventStoreClient.Append.cs | 25 +++- .../EventStoreClient.Delete.cs | 21 ++- .../EventStoreClient.Metadata.cs | 66 ++++++--- .../EventStoreClient.Read.cs | 12 +- .../EventStoreClient.Subscriptions.cs | 32 +++-- .../EventStoreClient.Tombstone.cs | 27 ++-- .../EventStoreClient.cs | 2 +- .../EventStoreClientExtensions.cs | 28 +++- .../StreamAppenderIdentifier.cs | 28 ++++ src/EventStore.Client/EventStoreClientBase.cs | 14 +- src/EventStore.Client/HttpFallback.cs | 8 +- src/EventStore.Client/SharingProvider.cs | 4 +- src/EventStore.Client/UserCredentials.cs | 18 +-- .../UserCertificateTests.cs | 125 +++++++++--------- .../TestCertificate.cs | 12 ++ .../TestCredentials.cs | 13 -- 29 files changed, 649 insertions(+), 317 deletions(-) rename .github/workflows/{enterprise.yml => ee.yml} (70%) create mode 100644 src/EventStore.Client.Streams/StreamAppenderIdentifier.cs create mode 100644 test/EventStore.Client.Tests.Common/TestCertificate.cs diff --git a/.github/workflows/enterprise.yml b/.github/workflows/ee.yml similarity index 70% rename from .github/workflows/enterprise.yml rename to .github/workflows/ee.yml index 6c5770cc5..90d5483dc 100644 --- a/.github/workflows/enterprise.yml +++ b/.github/workflows/ee.yml @@ -1,4 +1,4 @@ -name: Test CI +name: Test EE on: pull_request: @@ -14,6 +14,5 @@ jobs: with: docker-tag: 24.2.0-jammy docker-registry: docker.eventstore.com/eventstore-ee/eventstoredb-commercial - build-matrix: '["Streams", "PersistentSubscriptions", "Operations", "UserManagement", "ProjectionManagement"]' test-matrix: '["Plugins"]' secrets: inherit diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs index fc0e93996..2a9b9ab13 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Create.cs @@ -118,11 +118,21 @@ private static CreateReq.Types.AllOptions AllOptionsForCreateProto(Position posi /// Creates a persistent subscription. /// /// - public async Task CreateToStreamAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await CreateInternalAsync(streamName, groupName, null, settings, deadline, userCredentials, - cancellationToken) + public async Task CreateToStreamAsync( + string streamName, string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + await CreateInternalAsync( + streamName, + groupName, + null, + settings, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); /// @@ -130,36 +140,70 @@ await CreateInternalAsync(streamName, groupName, null, settings, deadline, userC /// /// [Obsolete("CreateAsync is no longer supported. Use CreateToStreamAsync instead.", false)] - public async Task CreateAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await CreateInternalAsync(streamName, groupName, null, settings, deadline, userCredentials, - cancellationToken) + public async Task CreateAsync( + string streamName, string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + await CreateInternalAsync( + streamName, + groupName, + null, + settings, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); /// /// Creates a filtered persistent subscription to $all. /// - public async Task CreateToAllAsync(string groupName, IEventFilter eventFilter, + public async Task CreateToAllAsync( + string groupName, IEventFilter eventFilter, PersistentSubscriptionSettings settings, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await CreateInternalAsync(SystemStreams.AllStream, groupName, eventFilter, settings, deadline, - userCredentials, cancellationToken) + UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + await CreateInternalAsync( + SystemStreams.AllStream, + groupName, + eventFilter, + settings, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); /// /// Creates a persistent subscription to $all. /// - public async Task CreateToAllAsync(string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await CreateInternalAsync(SystemStreams.AllStream, groupName, null, settings, deadline, userCredentials, - cancellationToken) + public async Task CreateToAllAsync( + string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + await CreateInternalAsync( + SystemStreams.AllStream, + groupName, + null, + settings, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); - private async Task CreateInternalAsync(string streamName, string groupName, IEventFilter? eventFilter, + private async Task CreateInternalAsync( + string streamName, string groupName, IEventFilter? eventFilter, PersistentSubscriptionSettings settings, TimeSpan? deadline, UserCredentials? userCredentials, - CancellationToken cancellationToken) { + UserCertificate? userCertificate, + CancellationToken cancellationToken + ) { if (streamName is null) { throw new ArgumentNullException(nameof(streamName)); } @@ -198,7 +242,7 @@ private async Task CreateInternalAsync(string streamName, string groupName, IEve "The specified consumer strategy is not supported, specify one of the SystemConsumerStrategies"); } - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs index 69ee8a400..89406ddac 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Delete.cs @@ -9,16 +9,23 @@ partial class EventStorePersistentSubscriptionsClient { /// Deletes a persistent subscription. /// [Obsolete("DeleteAsync is no longer supported. Use DeleteToStreamAsync instead.", false)] - public Task DeleteAsync(string streamName, string groupName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => - DeleteToStreamAsync(streamName, groupName, deadline, userCredentials, cancellationToken); + public Task DeleteAsync( + string streamName, string groupName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + DeleteToStreamAsync(streamName, groupName, deadline, userCredentials, userCertificate, cancellationToken); /// /// Deletes a persistent subscription. /// - public async Task DeleteToStreamAsync(string streamName, string groupName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task DeleteToStreamAsync( + string streamName, string groupName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { @@ -47,9 +54,19 @@ public async Task DeleteToStreamAsync(string streamName, string groupName, TimeS /// /// Deletes a persistent subscription to $all. /// - public async Task DeleteToAllAsync(string groupName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => - await DeleteToStreamAsync(SystemStreams.AllStream, groupName, deadline, userCredentials, cancellationToken) + public async Task DeleteToAllAsync( + string groupName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + await DeleteToStreamAsync( + SystemStreams.AllStream, + groupName, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); } } diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs index 696dd9c5f..3c7214487 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Info.cs @@ -10,9 +10,14 @@ partial class EventStorePersistentSubscriptionsClient { /// /// Gets the status of a persistent subscription to $all /// - public async Task GetInfoToAllAsync(string groupName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task GetInfoToAllAsync( + string groupName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { var req = new GetInfoReq() { Options = new GetInfoReq.Types.Options{ @@ -31,9 +36,14 @@ public async Task GetInfoToAllAsync(string groupName /// /// Gets the status of a persistent subscription to a stream /// - public async Task GetInfoToStreamAsync(string streamName, string groupName, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task GetInfoToStreamAsync( + string streamName, string groupName, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); + if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsGetInfo) { var req = new GetInfoReq() { Options = new GetInfoReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs index d80eabc9f..98d8c8e65 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.List.cs @@ -12,10 +12,13 @@ partial class EventStorePersistentSubscriptionsClient { /// /// Lists persistent subscriptions to $all. /// - public async Task> ListToAllAsync(TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task> ListToAllAsync( + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { var req = new ListReq() { Options = new ListReq.Types.Options{ @@ -35,10 +38,13 @@ public async Task> ListToAllAsync(TimeSp /// /// Lists persistent subscriptions to the specified stream. /// - public async Task> ListToStreamAsync(string streamName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task> ListToStreamAsync( + string streamName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { var req = new ListReq() { Options = new ListReq.Types.Options { @@ -59,10 +65,13 @@ public async Task> ListToStreamAsync(str /// /// Lists all persistent subscriptions. /// - public async Task> ListAllAsync(TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task> ListAllAsync( + TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsList) { var req = new ListReq() { Options = new ListReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs index cb9614e39..1372c24ff 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Read.cs @@ -12,18 +12,21 @@ partial class EventStorePersistentSubscriptionsClient { /// /// [Obsolete("SubscribeAsync is no longer supported. Use SubscribeToStream with manual acks instead.", false)] - public async Task SubscribeAsync(string streamName, string groupName, + public async Task SubscribeAsync( + string streamName, string groupName, Func eventAppeared, Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, bool autoAck = true, - CancellationToken cancellationToken = default) { + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, int bufferSize = 10, + bool autoAck = true, + CancellationToken cancellationToken = default + ) { if (autoAck) { throw new InvalidOperationException( $"AutoAck is no longer supported. Please use {nameof(SubscribeToStreamAsync)} with manual acks instead."); } return await SubscribeToStreamAsync(streamName, groupName, eventAppeared, subscriptionDropped, - userCredentials, bufferSize, cancellationToken).ConfigureAwait(false); + userCredentials, userCertificate, bufferSize, cancellationToken).ConfigureAwait(false); } /// @@ -33,13 +36,15 @@ public async Task SubscribeAsync(string streamName, stri /// /// [Obsolete("SubscribeToStreamAsync is no longer supported. Use SubscribeToStream with manual acks instead.", false)] - public async Task SubscribeToStreamAsync(string streamName, string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, - CancellationToken cancellationToken = default) { + public async Task SubscribeToStreamAsync( + string streamName, string groupName, + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, int bufferSize = 10, + CancellationToken cancellationToken = default + ) { return await PersistentSubscription - .Confirm(SubscribeToStream(streamName, groupName, bufferSize, userCredentials, cancellationToken), + .Confirm(SubscribeToStream(streamName, groupName, bufferSize, userCredentials, userCertificate, cancellationToken), eventAppeared, subscriptionDropped ?? delegate { }, _log, userCredentials, cancellationToken) .ConfigureAwait(false); } @@ -51,10 +56,14 @@ public async Task SubscribeToStreamAsync(string streamNa /// The name of the persistent subscription group. /// The size of the buffer. /// The optional user credentials to perform operation with. + /// The optional user certificate to perform operation with. /// The optional . /// - public PersistentSubscriptionResult SubscribeToStream(string streamName, string groupName, int bufferSize = 10, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + public PersistentSubscriptionResult SubscribeToStream( + string streamName, string groupName, int bufferSize = 10, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { if (streamName == null) { throw new ArgumentNullException(nameof(streamName)); } @@ -88,7 +97,7 @@ public PersistentSubscriptionResult SubscribeToStream(string streamName, string } return new PersistentSubscriptionResult(streamName, groupName, async ct => { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, ct).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, ct).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { @@ -103,13 +112,23 @@ public PersistentSubscriptionResult SubscribeToStream(string streamName, string /// Subscribes to a persistent subscription to $all. Messages must be manually acknowledged /// [Obsolete("SubscribeToAllAsync is no longer supported. Use SubscribeToAll with manual acks instead.", false)] - public async Task SubscribeToAllAsync(string groupName, - Func eventAppeared, - Action? subscriptionDropped = null, - UserCredentials? userCredentials = null, int bufferSize = 10, - CancellationToken cancellationToken = default) => - await SubscribeToStreamAsync(SystemStreams.AllStream, groupName, eventAppeared, subscriptionDropped, - userCredentials, bufferSize, cancellationToken) + public async Task SubscribeToAllAsync( + string groupName, + Func eventAppeared, + Action? subscriptionDropped = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, int bufferSize = 10, + CancellationToken cancellationToken = default + ) => + await SubscribeToStreamAsync( + SystemStreams.AllStream, + groupName, + eventAppeared, + subscriptionDropped, + userCredentials, + userCertificate, + bufferSize, + cancellationToken + ) .ConfigureAwait(false); /// @@ -118,11 +137,22 @@ await SubscribeToStreamAsync(SystemStreams.AllStream, groupName, eventAppeared, /// The name of the persistent subscription group. /// The size of the buffer. /// The optional user credentials to perform operation with. + /// The optional user certificate to perform operation with. /// The optional . /// - public PersistentSubscriptionResult SubscribeToAll(string groupName, int bufferSize = 10, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) => - SubscribeToStream(SystemStreams.AllStream, groupName, bufferSize, userCredentials, cancellationToken); + public PersistentSubscriptionResult SubscribeToAll( + string groupName, int bufferSize = 10, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + SubscribeToStream( + SystemStreams.AllStream, + groupName, + bufferSize, + userCredentials, + userCertificate, + cancellationToken + ); /// public class PersistentSubscriptionResult : IAsyncEnumerable, IAsyncDisposable, IDisposable { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs index ecdb9142a..c4a896ba8 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.ReplayParked.cs @@ -11,10 +11,13 @@ partial class EventStorePersistentSubscriptionsClient { /// /// Retry the parked messages of the persistent subscription /// - public async Task ReplayParkedMessagesToAllAsync(string groupName, long? stopAt = null, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task ReplayParkedMessagesToAllAsync( + string groupName, long? stopAt = null, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { var req = new ReplayParkedReq() { Options = new ReplayParkedReq.Types.Options{ @@ -43,10 +46,13 @@ await ReplayParkedHttpAsync(SystemStreams.AllStream, groupName, stopAt, channelI /// /// Retry the parked messages of the persistent subscription /// - public async Task ReplayParkedMessagesToStreamAsync(string streamName, string groupName, long? stopAt=null, - TimeSpan? deadline=null, UserCredentials? userCredentials=null, CancellationToken cancellationToken=default) { - - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task ReplayParkedMessagesToStreamAsync( + string streamName, string groupName, long? stopAt = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsReplayParked) { var req = new ReplayParkedReq() { Options = new ReplayParkedReq.Types.Options { diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs index cf2058bc5..d1b188ef0 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.RestartSubsystem.cs @@ -8,9 +8,12 @@ partial class EventStorePersistentSubscriptionsClient { /// /// Restarts the persistent subscriptions subsystem. /// - public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task RestartSubsystemAsync( + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); if (channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsRestartSubsystem) { await new PersistentSubscriptions.PersistentSubscriptions.PersistentSubscriptionsClient(channelInfo.CallInvoker) .RestartSubsystemAsync(new Empty(), EventStoreCallOptions diff --git a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs index 6cc407962..11d3da2e3 100644 --- a/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs +++ b/src/EventStore.Client.PersistentSubscriptions/EventStorePersistentSubscriptionsClient.Update.cs @@ -62,18 +62,30 @@ private static UpdateReq.Types.AllOptions AllOptionsForUpdateProto(Position posi /// /// [Obsolete("UpdateAsync is no longer supported. Use UpdateToStreamAsync instead.", false)] - public Task UpdateAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - UpdateToStreamAsync(streamName, groupName, settings, deadline, userCredentials, cancellationToken); + public Task UpdateAsync( + string streamName, string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + UpdateToStreamAsync( + streamName, + groupName, + settings, + deadline, + userCredentials, + userCertificate, + cancellationToken + ); /// /// Updates a persistent subscription. /// /// - public async Task UpdateToStreamAsync(string streamName, string groupName, PersistentSubscriptionSettings settings, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { + public async Task UpdateToStreamAsync( + string streamName, string groupName, PersistentSubscriptionSettings settings, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { if (streamName is null) { throw new ArgumentNullException(nameof(streamName)); } @@ -102,7 +114,7 @@ public async Task UpdateToStreamAsync(string streamName, string groupName, Persi $"{nameof(settings.StartFrom)} must be of type '{nameof(Position)}' when subscribing to {SystemStreams.AllStream}"); } - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); if (streamName == SystemStreams.AllStream && !channelInfo.ServerCapabilities.SupportsPersistentSubscriptionsToAll) { @@ -155,11 +167,21 @@ public async Task UpdateToStreamAsync(string streamName, string groupName, Persi /// /// Updates a persistent subscription to $all. /// - public async Task UpdateToAllAsync(string groupName, PersistentSubscriptionSettings settings, + public async Task UpdateToAllAsync( + string groupName, PersistentSubscriptionSettings settings, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - await UpdateToStreamAsync(SystemStreams.AllStream, groupName, settings, deadline, userCredentials, - cancellationToken) + UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + await UpdateToStreamAsync( + SystemStreams.AllStream, + groupName, + settings, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); } } diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs index 6949be63a..cee0fb0ad 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Control.cs @@ -11,11 +11,16 @@ public partial class EventStoreProjectionManagementClient { /// /// /// + /// /// /// - public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task EnableAsync( + string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).EnableAsync(new EnableReq { Options = new EnableReq.Types.Options { @@ -31,11 +36,16 @@ public async Task EnableAsync(string name, TimeSpan? deadline = null, UserCreden /// /// /// + /// /// /// - public async Task ResetAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task ResetAsync( + string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).ResetAsync(new ResetReq { Options = new ResetReq.Types.Options { @@ -52,11 +62,12 @@ public async Task ResetAsync(string name, TimeSpan? deadline = null, UserCredent /// /// /// + /// /// /// - public Task AbortAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + public Task AbortAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) => - DisableInternalAsync(name, false, deadline, userCredentials, cancellationToken); + DisableInternalAsync(name, false, deadline, userCredentials, userCertificate, cancellationToken); /// /// Disables a projection. Saves the projection's checkpoint. @@ -64,22 +75,37 @@ public Task AbortAsync(string name, TimeSpan? deadline = null, UserCredentials? /// /// /// + /// /// /// - public Task DisableAsync(string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => - DisableInternalAsync(name, true, deadline, userCredentials, cancellationToken); + public Task DisableAsync( + string name, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => + DisableInternalAsync( + name, + true, + deadline, + userCredentials, + userCertificate, + cancellationToken + ); /// /// Restarts the projection subsystem. /// /// /// + /// /// /// - public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task RestartSubsystemAsync( + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).RestartSubsystemAsync(new Empty(), EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); @@ -87,8 +113,8 @@ public async Task RestartSubsystemAsync(TimeSpan? deadline = null, UserCredentia } private async Task DisableInternalAsync(string name, bool writeCheckpoint, TimeSpan? deadline, - UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + UserCredentials? userCredentials, UserCertificate? userCertificate, CancellationToken cancellationToken) { + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).DisableAsync(new DisableReq { Options = new DisableReq.Types.Options { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs index b8a8f6303..819ff1be7 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Create.cs @@ -11,11 +11,17 @@ public partial class EventStoreProjectionManagementClient { /// /// /// + /// /// /// - public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task CreateOneTimeAsync( + string query, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); + using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { @@ -34,12 +40,16 @@ public async Task CreateOneTimeAsync(string query, TimeSpan? deadline = null, /// /// /// + /// /// /// - public async Task CreateContinuousAsync(string name, string query, bool trackEmittedStreams = false, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task CreateContinuousAsync( + string name, string query, bool trackEmittedStreams = false, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { @@ -60,11 +70,16 @@ public async Task CreateContinuousAsync(string name, string query, bool trackEmi /// /// /// + /// /// /// - public async Task CreateTransientAsync(string name, string query, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + public async Task CreateTransientAsync( + string name, string query, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).CreateAsync(new CreateReq { Options = new CreateReq.Types.Options { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs index 0b8031204..3ec44c595 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.State.cs @@ -18,12 +18,22 @@ public partial class EventStoreProjectionManagementClient { /// /// /// + /// /// /// - public async Task GetResultAsync(string name, string? partition = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var value = await GetResultInternalAsync(name, partition, deadline, userCredentials, cancellationToken) + public async Task GetResultAsync( + string name, string? partition = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var value = await GetResultInternalAsync( + name, + partition, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); #if NET @@ -48,14 +58,24 @@ public async Task GetResultAsync(string name, string? partition = /// /// /// + /// /// /// /// - public async Task GetResultAsync(string name, string? partition = null, - JsonSerializerOptions? serializerOptions = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var value = await GetResultInternalAsync(name, partition, deadline, userCredentials, cancellationToken) + public async Task GetResultAsync( + string name, string? partition = null, + JsonSerializerOptions? serializerOptions = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var value = await GetResultInternalAsync( + name, + partition, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); #if NET await using var stream = new MemoryStream(); @@ -71,9 +91,13 @@ public async Task GetResultAsync(string name, string? partition = null, return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions)!; } - private async ValueTask GetResultInternalAsync(string name, string? partition, - TimeSpan? deadline, UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + private async ValueTask GetResultInternalAsync( + string name, string? partition, + TimeSpan? deadline, UserCredentials? userCredentials, UserCertificate? userCertificate, + CancellationToken cancellationToken + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).ResultAsync(new ResultReq { Options = new ResultReq.Types.Options { @@ -93,12 +117,22 @@ private async ValueTask GetResultInternalAsync(string name, string? parti /// /// /// + /// /// /// - public async Task GetStateAsync(string name, string? partition = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { - var value = await GetStateInternalAsync(name, partition, deadline, userCredentials, cancellationToken) + public async Task GetStateAsync( + string name, string? partition = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var value = await GetStateInternalAsync( + name, + partition, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); #if NET @@ -123,13 +157,24 @@ public async Task GetStateAsync(string name, string? partition = n /// /// /// + /// /// /// /// - public async Task GetStateAsync(string name, string? partition = null, + public async Task GetStateAsync( + string name, string? partition = null, JsonSerializerOptions? serializerOptions = null, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { - var value = await GetStateInternalAsync(name, partition, deadline, userCredentials, cancellationToken) + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { + var value = await GetStateInternalAsync( + name, + partition, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); #if NET @@ -146,9 +191,12 @@ public async Task GetStateAsync(string name, string? partition = null, return JsonSerializer.Deserialize(stream.ToArray(), serializerOptions)!; } - private async ValueTask GetStateInternalAsync(string name, string? partition, TimeSpan? deadline, - UserCredentials? userCredentials, CancellationToken cancellationToken) { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + private async ValueTask GetStateInternalAsync( + string name, string? partition, TimeSpan? deadline, + UserCredentials? userCredentials, UserCertificate? userCertificate, CancellationToken cancellationToken + ) { + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).StateAsync(new StateReq { Options = new StateReq.Types.Options { diff --git a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs index 8b8169c89..28c7978f5 100644 --- a/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs +++ b/src/EventStore.Client.ProjectionManagement/EventStoreProjectionManagementClient.Update.cs @@ -13,11 +13,14 @@ public partial class EventStoreProjectionManagementClient { /// /// /// + /// /// /// - public async Task UpdateAsync(string name, string query, bool? emitEnabled = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { + public async Task UpdateAsync( + string name, string query, bool? emitEnabled = null, + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { var options = new UpdateReq.Types.Options { Name = name, Query = query @@ -28,7 +31,8 @@ public async Task UpdateAsync(string name, string query, bool? emitEnabled = nul options.NoEmitOptions = new Empty(); } - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Projections.Projections.ProjectionsClient( channelInfo.CallInvoker).UpdateAsync(new UpdateReq { Options = options diff --git a/src/EventStore.Client.Streams/EventStoreClient.Append.cs b/src/EventStore.Client.Streams/EventStoreClient.Append.cs index c5b6b7203..9800c6607 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Append.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Append.cs @@ -1,4 +1,8 @@ +using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using System.Threading.Channels; using Google.Protobuf; using EventStore.Client.Streams; @@ -16,6 +20,7 @@ public partial class EventStoreClient { /// An to configure the operation's options. /// /// The for the operation. + /// The for the operation. /// The optional . /// public async Task AppendToStreamAsync( @@ -25,6 +30,7 @@ public async Task AppendToStreamAsync( Action? configureOperationOptions = null, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) { var options = Settings.OperationOptions.Clone(); configureOperationOptions?.Invoke(options); @@ -33,10 +39,10 @@ public async Task AppendToStreamAsync( var batchAppender = _streamAppender; var task = - userCredentials == null && await batchAppender.IsUsable().ConfigureAwait(false) + userCredentials == null && userCertificate == null && await batchAppender.IsUsable().ConfigureAwait(false) ? batchAppender.Append(streamName, expectedRevision, eventData, deadline, cancellationToken) : AppendToStreamInternal( - (await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false)).CallInvoker, + (await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false)).CallInvoker, new AppendReq { Options = new AppendReq.Types.Options { StreamIdentifier = streamName, @@ -62,6 +68,7 @@ public async Task AppendToStreamAsync( /// An to configure the operation's options. /// /// The for the operation. + /// The for the operation. /// The optional . /// public async Task AppendToStreamAsync( @@ -71,6 +78,7 @@ public async Task AppendToStreamAsync( Action? configureOperationOptions = null, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) { var operationOptions = Settings.OperationOptions.Clone(); configureOperationOptions?.Invoke(operationOptions); @@ -79,10 +87,10 @@ public async Task AppendToStreamAsync( var batchAppender = _streamAppender; var task = - userCredentials == null && await batchAppender.IsUsable().ConfigureAwait(false) + userCredentials == null && userCertificate == null && await batchAppender.IsUsable().ConfigureAwait(false) ? batchAppender.Append(streamName, expectedState, eventData, deadline, cancellationToken) : AppendToStreamInternal( - (await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false)).CallInvoker, + (await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false)).CallInvoker, new AppendReq { Options = new AppendReq.Types.Options { StreamIdentifier = streamName @@ -234,9 +242,12 @@ private class StreamAppender : IDisposable { private readonly Task?> _callTask; - public StreamAppender(EventStoreClientSettings settings, - Task?> callTask, CancellationToken cancellationToken, - Action onException) { + public StreamAppender( + EventStoreClientSettings settings, + Task?> callTask, + CancellationToken cancellationToken, + Action onException + ) { _settings = settings; _callTask = callTask; _cancellationToken = cancellationToken; diff --git a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs b/src/EventStore.Client.Streams/EventStoreClient.Delete.cs index f71bd2394..22051d8d2 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Delete.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Delete.cs @@ -13,6 +13,7 @@ public partial class EventStoreClient { /// The expected of the stream being deleted. /// The maximum time to wait before terminating the call. /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// public Task DeleteAsync( @@ -20,13 +21,19 @@ public Task DeleteAsync( StreamRevision expectedRevision, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) => DeleteInternal(new DeleteReq { Options = new DeleteReq.Types.Options { StreamIdentifier = streamName, Revision = expectedRevision } - }, deadline, userCredentials, cancellationToken); + }, + deadline, + userCredentials, + userCertificate, + cancellationToken + ); /// /// Deletes a stream asynchronously. @@ -35,6 +42,7 @@ public Task DeleteAsync( /// The expected of the stream being deleted. /// The maximum time to wait before terminating the call. /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// public Task DeleteAsync( @@ -42,18 +50,25 @@ public Task DeleteAsync( StreamState expectedState, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) => DeleteInternal(new DeleteReq { Options = new DeleteReq.Types.Options { StreamIdentifier = streamName } - }.WithAnyStreamRevision(expectedState), deadline, userCredentials, cancellationToken); + }.WithAnyStreamRevision(expectedState), + deadline, + userCredentials, + userCertificate, + cancellationToken + ); private async Task DeleteInternal(DeleteReq request, TimeSpan? deadline, UserCredentials? userCredentials, + UserCertificate? userCertificate, CancellationToken cancellationToken) { _log.LogDebug("Deleting stream {streamName}.", request.Options.StreamIdentifier); - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Streams.Streams.StreamsClient( channelInfo.CallInvoker).DeleteAsync(request, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); diff --git a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs b/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs index 08ece5e80..bbacb3e6e 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Metadata.cs @@ -13,15 +13,28 @@ public partial class EventStoreClient { /// The name of the stream to read the metadata for. /// /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// - public async Task GetStreamMetadataAsync(string streamName, TimeSpan? deadline = null, - UserCredentials? userCredentials = null, CancellationToken cancellationToken = default) { + public async Task GetStreamMetadataAsync( + string streamName, TimeSpan? deadline = null, + UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { _log.LogDebug("Read stream metadata for {streamName}.", streamName); try { - var result = ReadStreamAsync(Direction.Backwards, SystemStreams.MetastreamOf(streamName), - StreamPosition.End, 1, false, deadline, userCredentials, cancellationToken); + var result = ReadStreamAsync( + Direction.Backwards, + SystemStreams.MetastreamOf(streamName), + StreamPosition.End, + 1, + false, + deadline, + userCredentials, + userCertificate, + cancellationToken + ); await foreach (var message in result.Messages.ConfigureAwait(false)) { if (message is not StreamMessage.Event(var resolvedEvent)) { continue; @@ -47,12 +60,15 @@ public async Task GetStreamMetadataAsync(string streamName /// An to configure the operation's options. /// /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// - public Task SetStreamMetadataAsync(string streamName, StreamState expectedState, + public Task SetStreamMetadataAsync( + string streamName, StreamState expectedState, StreamMetadata metadata, Action? configureOperationOptions = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { var options = Settings.OperationOptions.Clone(); configureOperationOptions?.Invoke(options); @@ -60,7 +76,7 @@ public Task SetStreamMetadataAsync(string streamName, StreamState Options = new AppendReq.Types.Options { StreamIdentifier = SystemStreams.MetastreamOf(streamName) } - }.WithAnyStreamRevision(expectedState), options, deadline, userCredentials, cancellationToken); + }.WithAnyStreamRevision(expectedState), options, deadline, userCredentials, userCertificate, cancellationToken); } /// @@ -72,21 +88,32 @@ public Task SetStreamMetadataAsync(string streamName, StreamState /// An to configure the operation's options. /// /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// - public Task SetStreamMetadataAsync(string streamName, StreamRevision expectedRevision, + public Task SetStreamMetadataAsync( + string streamName, StreamRevision expectedRevision, StreamMetadata metadata, Action? configureOperationOptions = null, - TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) { + TimeSpan? deadline = null, UserCredentials? userCredentials = null, UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) { var options = Settings.OperationOptions.Clone(); configureOperationOptions?.Invoke(options); - return SetStreamMetadataInternal(metadata, new AppendReq { - Options = new AppendReq.Types.Options { - StreamIdentifier = SystemStreams.MetastreamOf(streamName), - Revision = expectedRevision - } - }, options, deadline, userCredentials, cancellationToken); + return SetStreamMetadataInternal( + metadata, + new AppendReq { + Options = new AppendReq.Types.Options { + StreamIdentifier = SystemStreams.MetastreamOf(streamName), + Revision = expectedRevision + } + }, + options, + deadline, + userCredentials, + userCertificate, + cancellationToken + ); } private async Task SetStreamMetadataInternal(StreamMetadata metadata, @@ -94,9 +121,10 @@ private async Task SetStreamMetadataInternal(StreamMetadata metada EventStoreClientOperationOptions operationOptions, TimeSpan? deadline, UserCredentials? userCredentials, + UserCertificate? userCertificate, CancellationToken cancellationToken) { - - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = + await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); return await AppendToStreamInternal(channelInfo.CallInvoker, appendReq, new[] { new EventData(Uuid.NewUuid(), SystemEventTypes.StreamMetadata, JsonSerializer.SerializeToUtf8Bytes(metadata, StreamMetadataJsonSerializerOptions)), diff --git a/src/EventStore.Client.Streams/EventStoreClient.Read.cs b/src/EventStore.Client.Streams/EventStoreClient.Read.cs index 16c06c187..f2271077a 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Read.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Read.cs @@ -16,6 +16,7 @@ public partial class EventStoreClient { /// Whether to resolve LinkTo events automatically. /// /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// public ReadAllStreamResult ReadAllAsync( @@ -25,13 +26,14 @@ public ReadAllStreamResult ReadAllAsync( bool resolveLinkTos = false, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) { if (maxCount <= 0) { throw new ArgumentOutOfRangeException(nameof(maxCount)); } return new ReadAllStreamResult(async _ => { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new() { @@ -65,6 +67,7 @@ public ReadAllStreamResult ReadAllAsync( /// Whether to resolve LinkTo events automatically. /// /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// public ReadAllStreamResult ReadAllAsync( @@ -75,6 +78,7 @@ public ReadAllStreamResult ReadAllAsync( bool resolveLinkTos = false, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default ) { if (maxCount <= 0) { @@ -103,7 +107,7 @@ public ReadAllStreamResult ReadAllAsync( }; return new ReadAllStreamResult(async _ => { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, readReq, Settings, deadline, userCredentials, cancellationToken); } @@ -221,6 +225,7 @@ public async IAsyncEnumerator GetAsyncEnumerator( /// Whether to resolve LinkTo events automatically. /// /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// public ReadStreamResult ReadStreamAsync( @@ -231,6 +236,7 @@ public ReadStreamResult ReadStreamAsync( bool resolveLinkTos = false, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) { if (maxCount <= 0) { @@ -238,7 +244,7 @@ public ReadStreamResult ReadStreamAsync( } return new ReadStreamResult(async _ => { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new() { diff --git a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs index 8d214e4cc..9da9ced75 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Subscriptions.cs @@ -14,6 +14,7 @@ public partial class EventStoreClient { /// An action invoked if the subscription is dropped. /// The optional to apply. /// The optional user credentials to perform operation with. + /// The optional user certificate to perform operation with. /// The optional . /// [Obsolete("SubscribeToAllAsync is no longer supported. Use SubscribeToAll instead.", false)] @@ -24,8 +25,9 @@ public Task SubscribeToAllAsync( Action? subscriptionDropped = default, SubscriptionFilterOptions? filterOptions = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) => StreamSubscription.Confirm( - SubscribeToAll(start, resolveLinkTos, filterOptions, userCredentials, cancellationToken), + SubscribeToAll(start, resolveLinkTos, filterOptions, userCredentials, userCertificate, cancellationToken), eventAppeared, subscriptionDropped, _log, filterOptions?.CheckpointReached, cancellationToken: cancellationToken); @@ -36,6 +38,7 @@ public Task SubscribeToAllAsync( /// Whether to resolve LinkTo events automatically. /// The optional to apply. /// The optional user credentials to perform operation with. + /// The optional user certificate to perform operation with. /// The optional . /// public StreamSubscriptionResult SubscribeToAll( @@ -43,8 +46,9 @@ public StreamSubscriptionResult SubscribeToAll( bool resolveLinkTos = false, SubscriptionFilterOptions? filterOptions = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) => new(async _ => { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new ReadReq.Types.Options { @@ -66,17 +70,21 @@ public StreamSubscriptionResult SubscribeToAll( /// Whether to resolve LinkTo events automatically. /// An action invoked if the subscription is dropped. /// The optional user credentials to perform operation with. + /// The optional user credentials to perform operation with. /// The optional . /// [Obsolete("SubscribeToStreamAsync is no longer supported. Use SubscribeToStream instead.", false)] - public Task SubscribeToStreamAsync(string streamName, - FromStream start, - Func eventAppeared, - bool resolveLinkTos = false, - Action? subscriptionDropped = default, - UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => StreamSubscription.Confirm( - SubscribeToStream(streamName, start, resolveLinkTos, userCredentials, cancellationToken), + public Task SubscribeToStreamAsync( + string streamName, + FromStream start, + Func eventAppeared, + bool resolveLinkTos = false, + Action? subscriptionDropped = default, + UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => StreamSubscription.Confirm( + SubscribeToStream(streamName, start, resolveLinkTos, userCredentials, userCertificate, cancellationToken), eventAppeared, subscriptionDropped, _log, cancellationToken: cancellationToken); /// @@ -86,6 +94,7 @@ public Task SubscribeToStreamAsync(string streamName, /// The name of the stream to read events from. /// Whether to resolve LinkTo events automatically. /// The optional user credentials to perform operation with. + /// The optional user certificate to perform operation with. /// The optional . /// public StreamSubscriptionResult SubscribeToStream( @@ -93,8 +102,9 @@ public StreamSubscriptionResult SubscribeToStream( FromStream start, bool resolveLinkTos = false, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) => new(async _ => { - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); return channelInfo.CallInvoker; }, new ReadReq { Options = new ReadReq.Types.Options { diff --git a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs b/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs index d0b7b0b6a..3d56a41d0 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.Tombstone.cs @@ -13,6 +13,7 @@ public partial class EventStoreClient { /// The expected of the stream being deleted. /// /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// public Task TombstoneAsync( @@ -20,12 +21,13 @@ public Task TombstoneAsync( StreamRevision expectedRevision, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) => TombstoneInternal(new TombstoneReq { Options = new TombstoneReq.Types.Options { StreamIdentifier = streamName, Revision = expectedRevision } - }, deadline, userCredentials, cancellationToken); + }, deadline, userCredentials, userCertificate, cancellationToken); /// /// Tombstones a stream asynchronously. Note: Tombstoned streams can never be recreated. @@ -34,6 +36,7 @@ public Task TombstoneAsync( /// The expected of the stream being deleted. /// /// The optional to perform operation with. + /// The optional to perform operation with. /// The optional . /// public Task TombstoneAsync( @@ -41,17 +44,25 @@ public Task TombstoneAsync( StreamState expectedState, TimeSpan? deadline = null, UserCredentials? userCredentials = null, - CancellationToken cancellationToken = default) => TombstoneInternal(new TombstoneReq { - Options = new TombstoneReq.Types.Options { - StreamIdentifier = streamName - } - }.WithAnyStreamRevision(expectedState), deadline, userCredentials, cancellationToken); + UserCertificate? userCertificate = null, + CancellationToken cancellationToken = default + ) => TombstoneInternal( + new TombstoneReq { + Options = new TombstoneReq.Types.Options { + StreamIdentifier = streamName + } + }.WithAnyStreamRevision(expectedState), + deadline, + userCredentials, + userCertificate, + cancellationToken + ); private async Task TombstoneInternal(TombstoneReq request, TimeSpan? deadline, - UserCredentials? userCredentials, CancellationToken cancellationToken) { + UserCredentials? userCredentials, UserCertificate? userCertificate, CancellationToken cancellationToken) { _log.LogDebug("Tombstoning stream {streamName}.", request.Options.StreamIdentifier); - var channelInfo = await GetChannelInfo(userCredentials?.UserCertificate, cancellationToken).ConfigureAwait(false); + var channelInfo = await GetChannelInfo(userCertificate?.Certificate, cancellationToken).ConfigureAwait(false); using var call = new Streams.Streams.StreamsClient( channelInfo.CallInvoker).TombstoneAsync(request, EventStoreCallOptions.CreateNonStreaming(Settings, deadline, userCredentials, cancellationToken)); diff --git a/src/EventStore.Client.Streams/EventStoreClient.cs b/src/EventStore.Client.Streams/EventStoreClient.cs index 361e6e2d4..7b0f0e3e0 100644 --- a/src/EventStore.Client.Streams/EventStoreClient.cs +++ b/src/EventStore.Client.Streams/EventStoreClient.cs @@ -24,7 +24,7 @@ public sealed partial class EventStoreClient : EventStoreClientBase { SingleWriter = true, AllowSynchronousContinuations = true }; - + private readonly ILogger _log; private Lazy _streamAppenderLazy; private StreamAppender _streamAppender => _streamAppenderLazy.Value; diff --git a/src/EventStore.Client.Streams/EventStoreClientExtensions.cs b/src/EventStore.Client.Streams/EventStoreClientExtensions.cs index 85a78dbe5..afbc1a826 100644 --- a/src/EventStore.Client.Streams/EventStoreClientExtensions.cs +++ b/src/EventStore.Client.Streams/EventStoreClientExtensions.cs @@ -47,6 +47,7 @@ public static Task SetSystemSettingsAsync( /// /// /// + /// /// /// /// @@ -57,13 +58,22 @@ public static async Task ConditionalAppendToStreamAsync( IEnumerable eventData, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) { if (client == null) { throw new ArgumentNullException(nameof(client)); } try { - var result = await client.AppendToStreamAsync(streamName, expectedRevision, eventData, - options => options.ThrowOnAppendFailure = false, deadline, userCredentials, cancellationToken) + var result = await client.AppendToStreamAsync( + streamName, + expectedRevision, + eventData, + options => options.ThrowOnAppendFailure = false, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); return ConditionalWriteResult.FromWriteResult(result); } catch (StreamDeletedException) { @@ -80,6 +90,7 @@ public static async Task ConditionalAppendToStreamAsync( /// /// /// + /// /// /// /// @@ -90,13 +101,22 @@ public static async Task ConditionalAppendToStreamAsync( IEnumerable eventData, TimeSpan? deadline = null, UserCredentials? userCredentials = null, + UserCertificate? userCertificate = null, CancellationToken cancellationToken = default) { if (client == null) { throw new ArgumentNullException(nameof(client)); } try { - var result = await client.AppendToStreamAsync(streamName, expectedState, eventData, - options => options.ThrowOnAppendFailure = false, deadline, userCredentials, cancellationToken) + var result = await client.AppendToStreamAsync( + streamName, + expectedState, + eventData, + options => options.ThrowOnAppendFailure = false, + deadline, + userCredentials, + userCertificate, + cancellationToken + ) .ConfigureAwait(false); return ConditionalWriteResult.FromWriteResult(result); } catch (StreamDeletedException) { diff --git a/src/EventStore.Client.Streams/StreamAppenderIdentifier.cs b/src/EventStore.Client.Streams/StreamAppenderIdentifier.cs new file mode 100644 index 000000000..363f13ec7 --- /dev/null +++ b/src/EventStore.Client.Streams/StreamAppenderIdentifier.cs @@ -0,0 +1,28 @@ +using System.Security.Cryptography.X509Certificates; + +namespace EventStore.Client; + +internal class StreamAppenderIdentifier(X509Certificate2? userCertificate) : IEquatable { + X509Certificate2? UserCertificate { get; } = userCertificate; + + public bool Equals(StreamAppenderIdentifier? other) { + if (other == null) + return false; + + if (UserCertificate == null && other.UserCertificate == null) + return true; + + if (UserCertificate == null || other.UserCertificate == null) + return false; + + return UserCertificate.Equals(other.UserCertificate); + } + + public override bool Equals(object? obj) { + return Equals(obj as StreamAppenderIdentifier); + } + + public override int GetHashCode() { + return UserCertificate?.GetHashCode() ?? 0; + } +} diff --git a/src/EventStore.Client/EventStoreClientBase.cs b/src/EventStore.Client/EventStoreClientBase.cs index 61d233730..896433214 100644 --- a/src/EventStore.Client/EventStoreClientBase.cs +++ b/src/EventStore.Client/EventStoreClientBase.cs @@ -99,18 +99,12 @@ protected async ValueTask GetChannelInfo(CancellationToken cancella /// Gets the current channel info. - protected async ValueTask GetChannelInfo(X509Certificate2? userCertificate, CancellationToken cancellationToken) { - _httpFallback = new Lazy(() => new HttpFallback(Settings, userCertificate)); - - if (userCertificate == null) { - return await _channelInfoProvider.CurrentAsync.WithCancellation(cancellationToken) - .ConfigureAwait(false); - } - - return await _channelInfoProvider + protected async ValueTask GetChannelInfo( + X509Certificate2? userCertificate, CancellationToken cancellationToken + ) => + await _channelInfoProvider .GetAsync(new GrpcChannelInput(ReconnectionRequired.Rediscover.Instance, userCertificate)) .WithCancellation(cancellationToken).ConfigureAwait(false); - } /// /// Only exists so that we can manually trigger rediscovery in the tests diff --git a/src/EventStore.Client/HttpFallback.cs b/src/EventStore.Client/HttpFallback.cs index 3b42fc107..bb24d7a9d 100644 --- a/src/EventStore.Client/HttpFallback.cs +++ b/src/EventStore.Client/HttpFallback.cs @@ -13,7 +13,7 @@ internal class HttpFallback : IDisposable { private readonly UserCredentials? _defaultCredentials; private readonly string _addressScheme; - internal HttpFallback (EventStoreClientSettings settings, X509Certificate2? userCertificate = null) { + internal HttpFallback (EventStoreClientSettings settings) { _addressScheme = settings.ConnectivitySettings.ResolvedAddressOrDefault.Scheme; _defaultCredentials = settings.DefaultCredentials; @@ -22,11 +22,9 @@ internal HttpFallback (EventStoreClientSettings settings, X509Certificate2? user handler.ClientCertificateOptions = ClientCertificateOption.Manual; bool configureClientCert = settings.ConnectivitySettings.UserCertificate != null - || settings.ConnectivitySettings.TlsCaFile != null - || userCertificate != null; + || settings.ConnectivitySettings.TlsCaFile != null; - var certificate = userCertificate - ?? settings.ConnectivitySettings.UserCertificate + var certificate = settings.ConnectivitySettings.UserCertificate ?? settings.ConnectivitySettings.TlsCaFile; if (configureClientCert) { diff --git a/src/EventStore.Client/SharingProvider.cs b/src/EventStore.Client/SharingProvider.cs index 6ee224924..a50483a91 100644 --- a/src/EventStore.Client/SharingProvider.cs +++ b/src/EventStore.Client/SharingProvider.cs @@ -37,10 +37,10 @@ private readonly Func, Task> _factory; private readonly TimeSpan _factoryRetryDelay; - private TInput _previousInput; + private TInput _previousInput; private TaskCompletionSource _currentBox; private bool _disposed; - private readonly SemaphoreSlim _syncLock = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _syncLock = new SemaphoreSlim(1, 1); public SharingProvider( Func, Task> factory, diff --git a/src/EventStore.Client/UserCredentials.cs b/src/EventStore.Client/UserCredentials.cs index 5f85cea20..a57f3df12 100644 --- a/src/EventStore.Client/UserCredentials.cs +++ b/src/EventStore.Client/UserCredentials.cs @@ -1,5 +1,4 @@ using System.Net.Http.Headers; -using System.Security.Cryptography.X509Certificates; using System.Text; using static System.Convert; @@ -12,13 +11,6 @@ public class UserCredentials { // ReSharper disable once InconsistentNaming static readonly UTF8Encoding UTF8NoBom = new UTF8Encoding(false); - /// - /// Constructs a new . - /// - public UserCredentials(UserCertificate userCertificate) { - UserCertificate = userCertificate.Certificate; - } - /// /// Constructs a new . /// @@ -39,12 +31,7 @@ public UserCredentials(string bearerToken) { Authorization = new(Constants.Headers.BearerScheme, bearerToken); } - AuthenticationHeaderValue? Authorization { get; } - - /// - /// The client certificate - /// - public X509Certificate2? UserCertificate { get; } + AuthenticationHeaderValue Authorization { get; } /// /// The username @@ -57,8 +44,7 @@ public UserCredentials(string bearerToken) { public string? Password { get; } /// - public override string ToString() => - UserCertificate != null ? string.Empty : Authorization?.ToString() ?? string.Empty; + public override string ToString() => Authorization.ToString(); /// /// Implicitly convert a to a . diff --git a/test/EventStore.Client.Plugins.Tests/UserCertificateTests.cs b/test/EventStore.Client.Plugins.Tests/UserCertificateTests.cs index ec5e99f27..c27cafed7 100644 --- a/test/EventStore.Client.Plugins.Tests/UserCertificateTests.cs +++ b/test/EventStore.Client.Plugins.Tests/UserCertificateTests.cs @@ -3,7 +3,7 @@ namespace EventStore.Client.Plugins.Tests { public class UserCertificateTests(ITestOutputHelper output, EventStoreFixture fixture) : EventStoreTests(output, fixture) { [Fact] - public async Task user_credentials_takes_precedence_over_user_certificates() { + public async Task user_credentials_takes_precedence_over_user_certificate_on_a_call() { var certPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"); var certKeyPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key"); @@ -16,115 +16,108 @@ public async Task user_credentials_takes_precedence_over_user_certificates() { var client = new EventStoreClient(settings); - await client.AppendToStreamAsync( + var appendResult = await client.AppendToStreamAsync( stream, StreamState.Any, - Enumerable.Empty(), - userCredentials: TestCredentials.TestBadUser - ).ShouldThrowAsync(); + Fixture.CreateTestEvents(5), + userCredentials: new UserCredentials("admin", "changeit"), + userCertificate: TestCertificate.UserAdminCertificate + ); + + Assert.NotNull(appendResult); } [Fact] - public Task does_not_accept_certificates_with_invalid_path() { - var certPath = Path.Combine("invalid.crt"); - var certKeyPath = Path.Combine("invalid.key"); + public async Task invalid_user_credentials_takes_precedence_over_admin_cert() { + var certPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"); + var certKeyPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key"); var connectionString = - $"esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true&certPath={certPath}&certKeyPath={certKeyPath}"; + $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&certPath={certPath}&certKeyPath={certKeyPath}"; + + var stream = Fixture.GetStreamName(); + + var settings = EventStoreClientSettings.Create(connectionString); - Assert.Throws(() => EventStoreClientSettings.Create(connectionString)); + var client = new EventStoreClient(settings); - return Task.CompletedTask; + await client.AppendToStreamAsync( + stream, + StreamState.Any, + Fixture.CreateTestEvents(5), + userCredentials: TestCredentials.TestBadUser, + userCertificate: TestCertificate.UserAdminCertificate + ).ShouldThrowAsync(); } [Fact] - public async Task append_should_be_successful_with_user_certificates() { - var certPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"); - var certKeyPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key"); - - Assert.True(File.Exists(certPath)); - Assert.True(File.Exists(certKeyPath)); + public async Task valid_user_credentials_takes_precedence_over_invalid_user_cert_with_invalid_client() { + var certPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.crt"); + var certKeyPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.key"); var connectionString = $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&certPath={certPath}&certKeyPath={certKeyPath}"; - Fixture.Log.Information("connectionString: {connectionString}", connectionString); - var stream = Fixture.GetStreamName(); var settings = EventStoreClientSettings.Create(connectionString); var client = new EventStoreClient(settings); - var result = await client.AppendToStreamAsync( + await client.AppendToStreamAsync( stream, StreamState.Any, - Enumerable.Empty() - ); - - Assert.NotNull(result); + Fixture.CreateTestEvents(5), + userCredentials: new UserCredentials("admin", "changeit"), + userCertificate: TestCertificate.BadUserCertificate + ).ShouldThrowAsync(); } [Fact] - public async Task append_with_correct_user_certificate_but_read_with_bad_user_certificate() - { - var connectionString = "esdb://admin:changeit@localhost:2113/?tls=true&tlsVerifyCert=true"; - - var stream = Fixture.GetStreamName(); + public async Task overriding_invalid_client_with_valid_user_credentials_throws_unauthenticated() { + var certPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.crt"); + var certKeyPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.key"); - var settings = EventStoreClientSettings.Create(connectionString); + var connectionString = + $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&certPath={certPath}&certKeyPath={certKeyPath}"; - var client = new EventStoreClient(settings); + var stream = Fixture.GetStreamName(); - var appendResult = await client.AppendToStreamAsync( - stream, - StreamState.Any, - Enumerable.Empty(), - userCredentials: TestCredentials.UserAdminCertificate - ); + var settings = EventStoreClientSettings.Create(connectionString); - Assert.NotNull(appendResult); + var client = new EventStoreClient(settings); - await Fixture.Streams - .ReadStreamAsync( - Direction.Forwards, - stream, - StreamPosition.Start, - userCredentials: TestCredentials.BadUserCertificate - ) - .ShouldThrowAsync(); - } + await client.AppendToStreamAsync( + stream, + StreamState.Any, + Fixture.CreateTestEvents(5), + userCredentials: new UserCredentials("admin", "changeit") + ).ShouldThrowAsync(); + } [Fact] - public async Task overriding_user_certificate_with_basic_authentication_should_work() { + public async Task override_call_with_invalid_user_certificate_should_throw_unauthenticated() { var certPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"); var certKeyPath = Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key"); var connectionString = $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&certPath={certPath}&certKeyPath={certKeyPath}"; - var stream = Fixture.GetStreamName(); - - var settings = EventStoreClientSettings.Create(connectionString); + var stream = Fixture.GetStreamName(); - var client = new EventStoreClient(settings); + var settings = EventStoreClientSettings.Create(connectionString); - var appendResult = await client.AppendToStreamAsync( - stream, - StreamState.Any, - Fixture.CreateTestEvents(5), - userCredentials: new UserCredentials("admin", "changeit") - ); + var client = new EventStoreClient(settings); - Assert.NotNull(appendResult); + await client.AppendToStreamAsync( + stream, + StreamState.Any, + Fixture.CreateTestEvents(5), + userCertificate: TestCertificate.BadUserCertificate + ).ShouldThrowAsync(); - var readResult = await Fixture.Streams - .ReadStreamAsync( - Direction.Forwards, - stream, - StreamPosition.Start - ).CountAsync(); - readResult.ShouldBe(5); + await Fixture.Streams.ReadStreamAsync(Direction.Forwards, stream, StreamPosition.Start) + .ShouldThrowAsync(); } } } diff --git a/test/EventStore.Client.Tests.Common/TestCertificate.cs b/test/EventStore.Client.Tests.Common/TestCertificate.cs new file mode 100644 index 000000000..cfe55e92f --- /dev/null +++ b/test/EventStore.Client.Tests.Common/TestCertificate.cs @@ -0,0 +1,12 @@ +namespace EventStore.Client.Tests; + +public static class TestCertificate { + public static readonly UserCertificate UserAdminCertificate = new( + Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"), + Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key") + ); + public static readonly UserCertificate BadUserCertificate = new( + Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.crt"), + Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.key") + ); +} diff --git a/test/EventStore.Client.Tests.Common/TestCredentials.cs b/test/EventStore.Client.Tests.Common/TestCredentials.cs index 1a1d89d75..b3d075ba4 100644 --- a/test/EventStore.Client.Tests.Common/TestCredentials.cs +++ b/test/EventStore.Client.Tests.Common/TestCredentials.cs @@ -6,17 +6,4 @@ public static class TestCredentials { public static readonly UserCredentials TestUser2 = new("user2", "pa$$2"); public static readonly UserCredentials TestAdmin = new("adm", "admpa$$"); public static readonly UserCredentials TestBadUser = new("badlogin", "badpass"); - - public static readonly UserCredentials UserAdminCertificate = new( - new UserCertificate( - Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.crt"), - Path.Combine(Environment.CurrentDirectory, "certs", "user-admin", "user-admin.key") - ) - ); - public static readonly UserCredentials BadUserCertificate = new( - new UserCertificate( - Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.crt"), - Path.Combine(Environment.CurrentDirectory, "certs", "user-invalid", "user-invalid.key") - ) - ); }