diff --git a/src/Momento.Sdk/AuthClient.cs b/src/Momento.Sdk/AuthClient.cs index f871fdcb..e57efea5 100644 --- a/src/Momento.Sdk/AuthClient.cs +++ b/src/Momento.Sdk/AuthClient.cs @@ -19,7 +19,7 @@ public AuthClient(IAuthConfiguration config, ICredentialProvider authProvider) scsTokenClient = new ScsTokenClient(config, authProvider.AuthToken, authProvider.TokenEndpoint); } - public async Task GenerateDisposableTokenAsync(DisposableTokenScope scope, ExpiresIn expiresIn) + public async Task GenerateDisposableTokenAsync(DisposableTokenScope scope, ExpiresIn expiresIn, string? tokenId = null) { try { Utils.CheckValidDisposableTokenExpiry(expiresIn); @@ -28,7 +28,7 @@ public async Task GenerateDisposableTokenAsync( { return new GenerateDisposableTokenResponse.Error(new InvalidArgumentException(e.Message)); } - return await scsTokenClient.GenerateDisposableToken(scope, expiresIn); + return await scsTokenClient.GenerateDisposableToken(scope, expiresIn, tokenId); } /// diff --git a/src/Momento.Sdk/Exceptions/UnknownException.cs b/src/Momento.Sdk/Exceptions/UnknownException.cs index a0fc4619..145900ec 100644 --- a/src/Momento.Sdk/Exceptions/UnknownException.cs +++ b/src/Momento.Sdk/Exceptions/UnknownException.cs @@ -10,6 +10,6 @@ public class UnknownException : SdkException /// public UnknownException(string message, MomentoErrorTransportDetails? transportDetails = null, Exception? e = null) : base(MomentoErrorCode.UNKNOWN_ERROR, message, transportDetails, e) { - this.MessageWrapper = "Unknown error has occurred"; + this.MessageWrapper = "Unknown error has occurred: " + InnerException; } } diff --git a/src/Momento.Sdk/IAuthClient.cs b/src/Momento.Sdk/IAuthClient.cs index d0e63339..44a75cd6 100644 --- a/src/Momento.Sdk/IAuthClient.cs +++ b/src/Momento.Sdk/IAuthClient.cs @@ -8,5 +8,5 @@ namespace Momento.Sdk; public interface IAuthClient : IDisposable { public Task GenerateDisposableTokenAsync(DisposableTokenScope scope, - ExpiresIn expiresIn); + ExpiresIn expiresIn, string? tokenId = null); } diff --git a/src/Momento.Sdk/Internal/ScsTokenClient.cs b/src/Momento.Sdk/Internal/ScsTokenClient.cs index 7ad59102..23455efd 100644 --- a/src/Momento.Sdk/Internal/ScsTokenClient.cs +++ b/src/Momento.Sdk/Internal/ScsTokenClient.cs @@ -37,7 +37,7 @@ private DateTime CalculateDeadline() private const string RequestTypeAuthGenerateDisposableToken = "GENERATE_DISPOSABLE_TOKEN"; public async Task GenerateDisposableToken( - DisposableTokenScope scope, ExpiresIn expiresIn + DisposableTokenScope scope, ExpiresIn expiresIn, string? tokenId = null ) { Permissions permissions; try @@ -59,7 +59,8 @@ public async Task GenerateDisposableToken( { Expires = new _GenerateDisposableTokenRequest.Types.Expires() { ValidForSeconds = (uint)expiresIn.Seconds() }, AuthToken = this.authToken, - Permissions = permissions + Permissions = permissions, + TokenId = tokenId ?? "" }; _logger.LogTraceExecutingGenericRequest(RequestTypeAuthGenerateDisposableToken); var response = await grpcManager.Client.generateDisposableToken( diff --git a/src/Momento.Sdk/Internal/ScsTopicClient.cs b/src/Momento.Sdk/Internal/ScsTopicClient.cs index 502cd946..92d3d384 100644 --- a/src/Momento.Sdk/Internal/ScsTopicClient.cs +++ b/src/Momento.Sdk/Internal/ScsTopicClient.cs @@ -242,10 +242,10 @@ public async Task Subscribe() { case _TopicValue.KindOneofCase.Text: _logger.LogTraceTopicMessageReceived("text", _cacheName, _topicName); - return new TopicMessage.Text(message.Item.Value); + return new TopicMessage.Text(message.Item.Value, message.Item.PublisherId == "" ? null : message.Item.PublisherId); case _TopicValue.KindOneofCase.Binary: _logger.LogTraceTopicMessageReceived("binary", _cacheName, _topicName); - return new TopicMessage.Binary(message.Item.Value); + return new TopicMessage.Binary(message.Item.Value, message.Item.PublisherId == "" ? null : message.Item.PublisherId); case _TopicValue.KindOneofCase.None: default: _logger.LogTraceTopicMessageReceived("unknown", _cacheName, _topicName); diff --git a/src/Momento.Sdk/Momento.Sdk.csproj b/src/Momento.Sdk/Momento.Sdk.csproj index 474a251c..b837671f 100644 --- a/src/Momento.Sdk/Momento.Sdk.csproj +++ b/src/Momento.Sdk/Momento.Sdk.csproj @@ -55,7 +55,7 @@ - + diff --git a/src/Momento.Sdk/Responses/TopicMessage.cs b/src/Momento.Sdk/Responses/TopicMessage.cs index a9db1da5..2863864d 100644 --- a/src/Momento.Sdk/Responses/TopicMessage.cs +++ b/src/Momento.Sdk/Responses/TopicMessage.cs @@ -43,15 +43,22 @@ public class Text : TopicMessage /// /// A topic message containing a text value. /// - public Text(_TopicValue topicValue) + public Text(_TopicValue topicValue, string? tokenId = null) { Value = topicValue.Text; + TokenId = tokenId; } /// /// The text value of this message. /// public string Value { get; } + + /// + /// The TokenId that was used to publish the message, or null if the token did not have an id. + /// This can be used to securely identify the sender of a message. + /// + public string? TokenId { get; } } /// @@ -62,15 +69,22 @@ public class Binary : TopicMessage /// /// A topic message containing a binary value. /// - public Binary(_TopicValue topicValue) + public Binary(_TopicValue topicValue, string? tokenId = null) { Value = topicValue.Binary.ToByteArray(); + TokenId = tokenId; } /// /// The binary value of this message. /// public byte[] Value { get; } + + /// + /// The TokenId that was used to publish the message, or null if the token did not have an id. + /// This can be used to securely identify the sender of a message. + /// + public string? TokenId { get; } } /// diff --git a/tests/Integration/Momento.Sdk.Tests/AuthClientTopicTest.cs b/tests/Integration/Momento.Sdk.Tests/AuthClientTopicTest.cs index 08dd2afc..0ce694c5 100644 --- a/tests/Integration/Momento.Sdk.Tests/AuthClientTopicTest.cs +++ b/tests/Integration/Momento.Sdk.Tests/AuthClientTopicTest.cs @@ -32,7 +32,13 @@ public AuthClientTopicTest( private async Task GetClientForTokenScope(DisposableTokenScope scope) { - var response = await authClient.GenerateDisposableTokenAsync(scope, ExpiresIn.Minutes(2)); + return await GetClientForTokenScope(scope, null); + } + + + private async Task GetClientForTokenScope(DisposableTokenScope scope, string? tokenId) + { + var response = await authClient.GenerateDisposableTokenAsync(scope, ExpiresIn.Minutes(2), tokenId); Assert.True(response is GenerateDisposableTokenResponse.Success, $"Unexpected response: {response}"); string authToken = ""; if (response is GenerateDisposableTokenResponse.Success token) @@ -43,6 +49,7 @@ private async Task GetClientForTokenScope(DisposableTokenScope sco var authProvider = new StringMomentoTokenProvider(authToken); return new TopicClient(TopicConfigurations.Laptop.latest(), authProvider); } + private async Task PublishToTopic(string cache, string topic, string value, ITopicClient? client = null) { @@ -51,9 +58,17 @@ private async Task PublishToTopic(string cache, string topic, string value, ITop Assert.True(response is TopicPublishResponse.Success, $"Unexpected response: {response}"); } + private async Task ExpectTextFromSubscription( TopicSubscribeResponse.Subscription subscription, string expectedText ) + { + await ExpectTextFromSubscription(subscription, expectedText, null); + } + + private async Task ExpectTextFromSubscription( + TopicSubscribeResponse.Subscription subscription, string expectedText, string? tokenId + ) { var cts = new CancellationTokenSource(); cts.CancelAfter(5000); @@ -64,6 +79,7 @@ private async Task ExpectTextFromSubscription( Assert.True(message is TopicMessage.Text, $"Unexpected response: {message}"); if (message is TopicMessage.Text textMsg) { Assert.Equal(expectedText, textMsg.Value); + Assert.Equal(tokenId, textMsg.TokenId); gotText = true; } cts.Cancel(); @@ -338,6 +354,28 @@ public async Task GenerateDisposableTopicAuthToken_ReadWrite_HappyPath() ); await GenerateDisposableTopicAuthToken_ReadWrite_Common(readwriteTopicClient, messageValue); } + + + [Fact] + public async Task GenerateDisposableTopicAuthToken_ReadWrite_WithTokenId_HappyPath() + { + const string messageValue = "hello"; + const string tokenId = "tacoToken"; + var readwriteTopicClient = await GetClientForTokenScope( + DisposableTokenScopes.TopicPublishSubscribe(cacheName, topicName), + tokenId + ); + var subscribeResponse = await readwriteTopicClient.SubscribeAsync(cacheName, topicName); + Assert.True(subscribeResponse is TopicSubscribeResponse.Subscription, $"Unexpected response: {subscribeResponse}"); + var publishResponse = await readwriteTopicClient.PublishAsync(cacheName, topicName, messageValue); + Assert.True(publishResponse is TopicPublishResponse.Success, $"Unexpected response: {publishResponse}"); + if (subscribeResponse is TopicSubscribeResponse.Subscription subscription) + { + await PublishToTopic(cacheName, topicName, messageValue, readwriteTopicClient); + await ExpectTextFromSubscription(subscription, messageValue, tokenId); + } + } + [Fact] public async Task GenerateDisposableTopicAuthToken_ReadWrite_NamePrefix_HappyPath()