diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/CHANGELOG.md b/sdk/webpubsub/Azure.Messaging.WebPubSub/CHANGELOG.md index 29c3d38c0f508..113d3bba227d5 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/CHANGELOG.md +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/CHANGELOG.md @@ -4,6 +4,10 @@ ### Features Added +- Added method overloads `serviceClient.GetClientAccessUri`, `serviceClient.GetClientAccessUri` for MQTT clients. +- Added method `serviceClient.AddConnectionsToGroups` to add filtered connections to specified multiple groups. +- Added method `serviceClient.RemoveConnectionsFromGroups` to remove filtered connections from specified multiple groups. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/api/Azure.Messaging.WebPubSub.netstandard2.0.cs b/sdk/webpubsub/Azure.Messaging.WebPubSub/api/Azure.Messaging.WebPubSub.netstandard2.0.cs index 903575a7a73ff..bd877b6c98a49 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/api/Azure.Messaging.WebPubSub.netstandard2.0.cs +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/api/Azure.Messaging.WebPubSub.netstandard2.0.cs @@ -5,6 +5,11 @@ public static partial class ClientConnectionFilter public static string Create(System.FormattableString filter) { throw null; } public static string Create(System.FormattableString filter, System.IFormatProvider formatProvider) { throw null; } } + public enum WebPubSubClientProtocol + { + Default = 0, + Mqtt = 1, + } public enum WebPubSubPermission { SendToGroup = 1, @@ -22,6 +27,8 @@ public WebPubSubServiceClient(System.Uri endpoint, string hub, Azure.Core.TokenC public virtual System.Uri Endpoint { get { throw null; } } public virtual string Hub { get { throw null; } } public virtual Azure.Core.Pipeline.HttpPipeline Pipeline { get { throw null; } } + public virtual Azure.Response AddConnectionsToGroups(System.Collections.Generic.IEnumerable groups, string filter, Azure.RequestContext context = null) { throw null; } + public virtual System.Threading.Tasks.Task AddConnectionsToGroupsAsync(System.Collections.Generic.IEnumerable groups, string filter, Azure.RequestContext context = null) { throw null; } public virtual Azure.Response AddConnectionToGroup(string group, string connectionId, Azure.RequestContext context = null) { throw null; } public virtual System.Threading.Tasks.Task AddConnectionToGroupAsync(string group, string connectionId, Azure.RequestContext context = null) { throw null; } public virtual Azure.Response AddUserToGroup(string group, string userId, Azure.RequestContext context = null) { throw null; } @@ -38,13 +45,25 @@ public WebPubSubServiceClient(System.Uri endpoint, string hub, Azure.Core.TokenC public virtual System.Threading.Tasks.Task CloseUserConnectionsAsync(string userId, System.Collections.Generic.IEnumerable excluded = null, string reason = null, Azure.RequestContext context = null) { throw null; } public virtual Azure.Response ConnectionExists(string connectionId, Azure.RequestContext context = null) { throw null; } public virtual System.Threading.Tasks.Task> ConnectionExistsAsync(string connectionId, Azure.RequestContext context = null) { throw null; } - public virtual System.Uri GetClientAccessUri(System.DateTimeOffset expiresAt, string userId = null, System.Collections.Generic.IEnumerable roles = null, System.Collections.Generic.IEnumerable groups = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Uri GetClientAccessUri(System.DateTimeOffset expiresAt, string userId = null, System.Collections.Generic.IEnumerable roles = null, System.Collections.Generic.IEnumerable groups = null, Azure.Messaging.WebPubSub.WebPubSubClientProtocol clientProtocol = Azure.Messaging.WebPubSub.WebPubSubClientProtocol.Default, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Uri GetClientAccessUri(System.DateTimeOffset expiresAt, string userId, System.Collections.Generic.IEnumerable roles, System.Collections.Generic.IEnumerable groups, System.Threading.CancellationToken cancellationToken) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public virtual System.Uri GetClientAccessUri(System.DateTimeOffset expiresAt, string userId, System.Collections.Generic.IEnumerable roles, System.Threading.CancellationToken cancellationToken) { throw null; } - public virtual System.Uri GetClientAccessUri(System.TimeSpan expiresAfter = default(System.TimeSpan), string userId = null, System.Collections.Generic.IEnumerable roles = null, System.Collections.Generic.IEnumerable groups = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Uri GetClientAccessUri(System.TimeSpan expiresAfter = default(System.TimeSpan), string userId = null, System.Collections.Generic.IEnumerable roles = null, System.Collections.Generic.IEnumerable groups = null, Azure.Messaging.WebPubSub.WebPubSubClientProtocol clientProtocol = Azure.Messaging.WebPubSub.WebPubSubClientProtocol.Default, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Uri GetClientAccessUri(System.TimeSpan expiresAfter, string userId, System.Collections.Generic.IEnumerable roles, System.Collections.Generic.IEnumerable groups, System.Threading.CancellationToken cancellationToken) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public virtual System.Uri GetClientAccessUri(System.TimeSpan expiresAfter, string userId, System.Collections.Generic.IEnumerable roles, System.Threading.CancellationToken cancellationToken) { throw null; } - public virtual System.Threading.Tasks.Task GetClientAccessUriAsync(System.DateTimeOffset expiresAt, string userId = null, System.Collections.Generic.IEnumerable roles = null, System.Collections.Generic.IEnumerable groups = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Threading.Tasks.Task GetClientAccessUriAsync(System.DateTimeOffset expiresAt, string userId = null, System.Collections.Generic.IEnumerable roles = null, System.Collections.Generic.IEnumerable groups = null, Azure.Messaging.WebPubSub.WebPubSubClientProtocol clientProtocol = Azure.Messaging.WebPubSub.WebPubSubClientProtocol.Default, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task GetClientAccessUriAsync(System.DateTimeOffset expiresAt, string userId, System.Collections.Generic.IEnumerable roles, System.Collections.Generic.IEnumerable groups, System.Threading.CancellationToken cancellationToken) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public virtual System.Threading.Tasks.Task GetClientAccessUriAsync(System.DateTimeOffset expiresAt, string userId, System.Collections.Generic.IEnumerable roles, System.Threading.CancellationToken cancellationToken) { throw null; } - public virtual System.Threading.Tasks.Task GetClientAccessUriAsync(System.TimeSpan expiresAfter = default(System.TimeSpan), string userId = null, System.Collections.Generic.IEnumerable roles = null, System.Collections.Generic.IEnumerable groups = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Threading.Tasks.Task GetClientAccessUriAsync(System.TimeSpan expiresAfter = default(System.TimeSpan), string userId = null, System.Collections.Generic.IEnumerable roles = null, System.Collections.Generic.IEnumerable groups = null, Azure.Messaging.WebPubSub.WebPubSubClientProtocol clientProtocol = Azure.Messaging.WebPubSub.WebPubSubClientProtocol.Default, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public virtual System.Threading.Tasks.Task GetClientAccessUriAsync(System.TimeSpan expiresAfter, string userId, System.Collections.Generic.IEnumerable roles, System.Collections.Generic.IEnumerable groups, System.Threading.CancellationToken cancellationToken) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public virtual System.Threading.Tasks.Task GetClientAccessUriAsync(System.TimeSpan expiresAfter, string userId, System.Collections.Generic.IEnumerable roles, System.Threading.CancellationToken cancellationToken) { throw null; } public virtual Azure.Response GrantPermission(Azure.Messaging.WebPubSub.WebPubSubPermission permission, string connectionId, string targetName = null, Azure.RequestContext context = null) { throw null; } public virtual System.Threading.Tasks.Task GrantPermissionAsync(Azure.Messaging.WebPubSub.WebPubSubPermission permission, string connectionId, string targetName = null, Azure.RequestContext context = null) { throw null; } @@ -54,6 +73,8 @@ public WebPubSubServiceClient(System.Uri endpoint, string hub, Azure.Core.TokenC public virtual System.Threading.Tasks.Task RemoveConnectionFromAllGroupsAsync(string connectionId, Azure.RequestContext context = null) { throw null; } public virtual Azure.Response RemoveConnectionFromGroup(string group, string connectionId, Azure.RequestContext context = null) { throw null; } public virtual System.Threading.Tasks.Task RemoveConnectionFromGroupAsync(string group, string connectionId, Azure.RequestContext context = null) { throw null; } + public virtual Azure.Response RemoveConnectionsFromGroups(System.Collections.Generic.IEnumerable groups, string filter = null, Azure.RequestContext context = null) { throw null; } + public virtual System.Threading.Tasks.Task RemoveConnectionsFromGroupsAsync(System.Collections.Generic.IEnumerable groups, string filter = null, Azure.RequestContext context = null) { throw null; } public virtual Azure.Response RemoveUserFromAllGroups(string userId, Azure.RequestContext context = null) { throw null; } public virtual System.Threading.Tasks.Task RemoveUserFromAllGroupsAsync(string userId, Azure.RequestContext context = null) { throw null; } public virtual Azure.Response RemoveUserFromGroup(string group, string userId, Azure.RequestContext context = null) { throw null; } @@ -87,11 +108,12 @@ public WebPubSubServiceClient(System.Uri endpoint, string hub, Azure.Core.TokenC } public partial class WebPubSubServiceClientOptions : Azure.Core.ClientOptions { - public WebPubSubServiceClientOptions(Azure.Messaging.WebPubSub.WebPubSubServiceClientOptions.ServiceVersion version = Azure.Messaging.WebPubSub.WebPubSubServiceClientOptions.ServiceVersion.V2021_10_01) { } + public WebPubSubServiceClientOptions(Azure.Messaging.WebPubSub.WebPubSubServiceClientOptions.ServiceVersion version = Azure.Messaging.WebPubSub.WebPubSubServiceClientOptions.ServiceVersion.V2024_01_01) { } public System.Uri ReverseProxyEndpoint { get { throw null; } set { } } public enum ServiceVersion { V2021_10_01 = 1, + V2024_01_01 = 2, } } } diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/assets.json b/sdk/webpubsub/Azure.Messaging.WebPubSub/assets.json index e0b58bff78499..e64dce29305c9 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/assets.json +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/webpubsub/Azure.Messaging.WebPubSub", - "Tag": "net/webpubsub/Azure.Messaging.WebPubSub_1d3c499946" + "Tag": "net/webpubsub/Azure.Messaging.WebPubSub_7d6164a51f" } diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/Azure.Messaging.WebPubSub.csproj b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/Azure.Messaging.WebPubSub.csproj index d326eb3323a27..cf2818ed87b26 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/Azure.Messaging.WebPubSub.csproj +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/Azure.Messaging.WebPubSub.csproj @@ -1,4 +1,4 @@ - + Azure SDK client library for the WebPubSub service Azure SDK for WebPubSub diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/Generated/WebPubSubServiceClient.cs b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/Generated/WebPubSubServiceClient.cs index cff76222e6685..c413f7de0c9ee 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/Generated/WebPubSubServiceClient.cs +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/Generated/WebPubSubServiceClient.cs @@ -33,6 +33,72 @@ protected WebPubSubServiceClient() { } + /// + /// [Protocol Method] Add filtered connections to multiple groups. + /// + /// + /// + /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios. + /// + /// + /// + /// + /// The content to send as the body of the request. + /// The request context, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// Service returned a non-success status code. + /// The response returned from the service. + internal virtual async Task AddConnectionsToGroupsAsync(RequestContent content, RequestContext context = null) + { + Argument.AssertNotNull(content, nameof(content)); + + using var scope = ClientDiagnostics.CreateScope("WebPubSubServiceClient.AddConnectionsToGroups"); + scope.Start(); + try + { + using HttpMessage message = CreateAddConnectionsToGroupsRequest(content, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } + catch (Exception e) + { + scope.Failed(e); + throw; + } + } + + /// + /// [Protocol Method] Add filtered connections to multiple groups. + /// + /// + /// + /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios. + /// + /// + /// + /// + /// The content to send as the body of the request. + /// The request context, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// Service returned a non-success status code. + /// The response returned from the service. + internal virtual Response AddConnectionsToGroups(RequestContent content, RequestContext context = null) + { + Argument.AssertNotNull(content, nameof(content)); + + using var scope = ClientDiagnostics.CreateScope("WebPubSubServiceClient.AddConnectionsToGroups"); + scope.Start(); + try + { + using HttpMessage message = CreateAddConnectionsToGroupsRequest(content, context); + return _pipeline.ProcessMessage(message, context); + } + catch (Exception e) + { + scope.Failed(e); + throw; + } + } + /// /// [Protocol Method] Close the connections in the hub. /// @@ -111,16 +177,17 @@ public virtual Response CloseAllConnections(IEnumerable excluded = null, /// Roles that the connection with the generated token will have. /// The expire time of the generated token. /// Groups that the connection will join when it connects. + /// The type of client. Case-insensitive. If not set, it's "Default". For Web PubSub for Socket.IO, only the default value is supported. For Web PubSub, the valid values are 'Default' and 'MQTT'. Allowed values: "Default" | "MQTT". /// The request context, which can override default behaviors of the client pipeline on a per-call basis. /// Service returned a non-success status code. /// The response returned from the service. - internal virtual async Task GenerateClientTokenImplAsync(string userId = null, IEnumerable role = null, int? minutesToExpire = null, IEnumerable group = null, RequestContext context = null) + internal virtual async Task GenerateClientTokenImplAsync(string userId = null, IEnumerable role = null, int? minutesToExpire = null, IEnumerable group = null, string clientType = null, RequestContext context = null) { using var scope = ClientDiagnostics.CreateScope("WebPubSubServiceClient.GenerateClientTokenImpl"); scope.Start(); try { - using HttpMessage message = CreateGenerateClientTokenImplRequest(userId, role, minutesToExpire, group, context); + using HttpMessage message = CreateGenerateClientTokenImplRequest(userId, role, minutesToExpire, group, clientType, context); return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); } catch (Exception e) @@ -144,16 +211,83 @@ internal virtual async Task GenerateClientTokenImplAsync(string userId /// Roles that the connection with the generated token will have. /// The expire time of the generated token. /// Groups that the connection will join when it connects. + /// The type of client. Case-insensitive. If not set, it's "Default". For Web PubSub for Socket.IO, only the default value is supported. For Web PubSub, the valid values are 'Default' and 'MQTT'. Allowed values: "Default" | "MQTT". /// The request context, which can override default behaviors of the client pipeline on a per-call basis. /// Service returned a non-success status code. /// The response returned from the service. - internal virtual Response GenerateClientTokenImpl(string userId = null, IEnumerable role = null, int? minutesToExpire = null, IEnumerable group = null, RequestContext context = null) + internal virtual Response GenerateClientTokenImpl(string userId = null, IEnumerable role = null, int? minutesToExpire = null, IEnumerable group = null, string clientType = null, RequestContext context = null) { using var scope = ClientDiagnostics.CreateScope("WebPubSubServiceClient.GenerateClientTokenImpl"); scope.Start(); try { - using HttpMessage message = CreateGenerateClientTokenImplRequest(userId, role, minutesToExpire, group, context); + using HttpMessage message = CreateGenerateClientTokenImplRequest(userId, role, minutesToExpire, group, clientType, context); + return _pipeline.ProcessMessage(message, context); + } + catch (Exception e) + { + scope.Failed(e); + throw; + } + } + + /// + /// [Protocol Method] Remove filtered connections from multiple groups. + /// + /// + /// + /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios. + /// + /// + /// + /// + /// The content to send as the body of the request. + /// The request context, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// Service returned a non-success status code. + /// The response returned from the service. + internal virtual async Task RemoveConnectionsFromGroupsAsync(RequestContent content, RequestContext context = null) + { + Argument.AssertNotNull(content, nameof(content)); + + using var scope = ClientDiagnostics.CreateScope("WebPubSubServiceClient.RemoveConnectionsFromGroups"); + scope.Start(); + try + { + using HttpMessage message = CreateRemoveConnectionsFromGroupsRequest(content, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } + catch (Exception e) + { + scope.Failed(e); + throw; + } + } + + /// + /// [Protocol Method] Remove filtered connections from multiple groups. + /// + /// + /// + /// This protocol method allows explicit creation of the request and processing of the response for advanced scenarios. + /// + /// + /// + /// + /// The content to send as the body of the request. + /// The request context, which can override default behaviors of the client pipeline on a per-call basis. + /// is null. + /// Service returned a non-success status code. + /// The response returned from the service. + internal virtual Response RemoveConnectionsFromGroups(RequestContent content, RequestContext context = null) + { + Argument.AssertNotNull(content, nameof(content)); + + using var scope = ClientDiagnostics.CreateScope("WebPubSubServiceClient.RemoveConnectionsFromGroups"); + scope.Start(); + try + { + using HttpMessage message = CreateRemoveConnectionsFromGroupsRequest(content, context); return _pipeline.ProcessMessage(message, context); } catch (Exception e) @@ -1405,6 +1539,24 @@ public virtual Response RemoveUserFromAllGroups(string userId, RequestContext co } } + internal HttpMessage CreateAddConnectionsToGroupsRequest(RequestContent content, RequestContext context) + { + var message = _pipeline.CreateMessage(context, ResponseClassifier200); + var request = message.Request; + request.Method = RequestMethod.Post; + var uri = new RawRequestUriBuilder(); + uri.AppendRaw(_endpoint, false); + uri.AppendPath("/api/hubs/", false); + uri.AppendPath(_hub, true); + uri.AppendPath("/:addToGroups", false); + uri.AppendQuery("api-version", _apiVersion, true); + request.Uri = uri; + request.Headers.Add("Accept", "application/json"); + request.Headers.Add("Content-Type", "application/json"); + request.Content = content; + return message; + } + internal HttpMessage CreateCloseAllConnectionsRequest(IEnumerable excluded, string reason, RequestContext context) { var message = _pipeline.CreateMessage(context, ResponseClassifier204); @@ -1432,7 +1584,7 @@ internal HttpMessage CreateCloseAllConnectionsRequest(IEnumerable exclud return message; } - internal HttpMessage CreateGenerateClientTokenImplRequest(string userId, IEnumerable role, int? minutesToExpire, IEnumerable group, RequestContext context) + internal HttpMessage CreateGenerateClientTokenImplRequest(string userId, IEnumerable role, int? minutesToExpire, IEnumerable group, string clientType, RequestContext context) { var message = _pipeline.CreateMessage(context, ResponseClassifier200); var request = message.Request; @@ -1465,11 +1617,33 @@ internal HttpMessage CreateGenerateClientTokenImplRequest(string userId, IEnumer uri.AppendQuery("group", param, true); } } + if (clientType != null) + { + uri.AppendQuery("clientType", clientType, true); + } request.Uri = uri; request.Headers.Add("Accept", "application/json, text/json"); return message; } + internal HttpMessage CreateRemoveConnectionsFromGroupsRequest(RequestContent content, RequestContext context) + { + var message = _pipeline.CreateMessage(context, ResponseClassifier200); + var request = message.Request; + request.Method = RequestMethod.Post; + var uri = new RawRequestUriBuilder(); + uri.AppendRaw(_endpoint, false); + uri.AppendPath("/api/hubs/", false); + uri.AppendPath(_hub, true); + uri.AppendPath("/:removeFromGroups", false); + uri.AppendQuery("api-version", _apiVersion, true); + request.Uri = uri; + request.Headers.Add("Accept", "application/json"); + request.Headers.Add("Content-Type", "application/json"); + request.Content = content; + return message; + } + internal HttpMessage CreateSendToAllRequest(RequestContent content, ContentType contentType, IEnumerable excluded, string filter, RequestContext context) { var message = _pipeline.CreateMessage(context, ResponseClassifier202); @@ -1881,10 +2055,10 @@ internal HttpMessage CreateAddUserToGroupRequest(string userId, string group, Re return message; } - private static ResponseClassifier _responseClassifier204; - private static ResponseClassifier ResponseClassifier204 => _responseClassifier204 ??= new StatusCodeClassifier(stackalloc ushort[] { 204 }); private static ResponseClassifier _responseClassifier200; private static ResponseClassifier ResponseClassifier200 => _responseClassifier200 ??= new StatusCodeClassifier(stackalloc ushort[] { 200 }); + private static ResponseClassifier _responseClassifier204; + private static ResponseClassifier ResponseClassifier204 => _responseClassifier204 ??= new StatusCodeClassifier(stackalloc ushort[] { 204 }); private static ResponseClassifier _responseClassifier202; private static ResponseClassifier ResponseClassifier202 => _responseClassifier202 ??= new StatusCodeClassifier(stackalloc ushort[] { 202 }); private static ResponseClassifier _responseClassifier200404; diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubClientProtocol.cs b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubClientProtocol.cs new file mode 100644 index 0000000000000..ea59d637dc07b --- /dev/null +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubClientProtocol.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Messaging.WebPubSub; + +/// +/// The client protocol. +/// +public enum WebPubSubClientProtocol +{ + /// + /// Default client protocol, whose access endpoint starts with "/client". + /// + Default, + + /// + /// MQTT client protocol, whose access endpoint starts with "/clients/mqtt". + /// + Mqtt, +} diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClientOptions.ServiceVersion.cs b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClientOptions.ServiceVersion.cs index a791880e06163..b44536ed6cd09 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClientOptions.ServiceVersion.cs +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClientOptions.ServiceVersion.cs @@ -26,19 +26,28 @@ public enum ServiceVersion #pragma warning disable CA1707 // Identifiers should not contain underscores /// The 2021_10_01_stable version of the Azure WebPubSub service. V2021_10_01 = 1, + /// + /// The 2024_01_01_stable version of the Azure WebPubSub service. + /// + V2024_01_01 = 2, #pragma warning restore CA1707 // Identifiers should not contain underscores } /// /// The Latest supported by this client library. /// - private const ServiceVersion LatestVersion = ServiceVersion.V2021_10_01; + private const ServiceVersion LatestVersion = ServiceVersion.V2024_01_01; /// /// Gets the version of the service API used when making requests. /// internal string Version { get; } + /// + /// Gets the version enum of the service API used when making requests. + /// + internal ServiceVersion VersionEnum { get; } + /// Initializes a new instance of the . /// /// An optional to specify the version of the REST API to use. @@ -51,6 +60,7 @@ public enum ServiceVersion /// public WebPubSubServiceClientOptions(ServiceVersion version = LatestVersion) { + VersionEnum = version; Version = version.ToVersionString(); } } @@ -74,6 +84,7 @@ public static string ToVersionString(this WebPubSubServiceClientOptions.ServiceV version switch { WebPubSubServiceClientOptions.ServiceVersion.V2021_10_01 => "2021-10-01", + WebPubSubServiceClientOptions.ServiceVersion.V2024_01_01 => "2024-01-01", _ => throw CreateInvalidVersionException(version) }; diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClient_extensions.cs b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClient_extensions.cs index bc89dc5b8a36b..53417ab8c5a95 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClient_extensions.cs +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClient_extensions.cs @@ -18,7 +18,7 @@ namespace Azure.Messaging.WebPubSub [CodeGenSuppress("SendToConnection", typeof(string), typeof(RequestContent), typeof(RequestContext))] [CodeGenSuppress("SendToConnectionAsync", typeof(string), typeof(RequestContent), typeof(RequestContext))] [CodeGenSuppress("SendToGroup", typeof(string), typeof(RequestContent), typeof(IEnumerable), typeof(RequestContext))] - [CodeGenSuppress("SendToGroupAsync", typeof(string), typeof(RequestContent), typeof(IEnumerable), typeof(RequestContext))] + [CodeGenSuppress("SendToGroupAsync", typeof(string), typeof(RequestContent), typeof(IEnumerable), typeof(RequestContext))] [CodeGenSuppress("SendToUser", typeof(string), typeof(RequestContent), typeof(RequestContext))] [CodeGenSuppress("SendToUserAsync", typeof(string), typeof(RequestContent), typeof(RequestContext))] [CodeGenSuppress("AddUserToGroup", typeof(string), typeof(string), typeof(RequestContext))] @@ -27,6 +27,7 @@ namespace Azure.Messaging.WebPubSub [CodeGenSuppress("RemoveUserFromGroupAsync", typeof(string), typeof(string), typeof(RequestContext))] public partial class WebPubSubServiceClient { + private readonly WebPubSubServiceClientOptions.ServiceVersion _apiVersionEnum; private AzureKeyCredential _credential; private TokenCredential _tokenCredential; @@ -160,6 +161,7 @@ private WebPubSubServiceClient(Uri endpoint, string hub, WebPubSubServiceClientO options ??= new WebPubSubServiceClientOptions(); ClientDiagnostics = new ClientDiagnostics(options, true); _apiVersion = options.Version; + _apiVersionEnum = options.VersionEnum; } /// Broadcast message to all the connected client connections. @@ -170,7 +172,8 @@ public virtual async Task SendToAllAsync(string content, ContentType c { Argument.AssertNotNull(content, nameof(content)); - if (contentType == default) contentType = ContentType.TextPlain; + if (contentType == default) + contentType = ContentType.TextPlain; return await SendToAllAsync(RequestContent.Create(content), contentType.ToString(), default, context: default).ConfigureAwait(false); } @@ -183,7 +186,8 @@ public virtual Response SendToAll(string content, ContentType contentType = defa { Argument.AssertNotNull(content, nameof(content)); - if (contentType == default) contentType = ContentType.TextPlain; + if (contentType == default) + contentType = ContentType.TextPlain; return SendToAll(RequestContent.Create(content), contentType, excluded: default, context: default); } @@ -200,7 +204,8 @@ public virtual async Task SendToUserAsync(string userId, string conten Argument.AssertNotNull(userId, nameof(userId)); Argument.AssertNotNull(content, nameof(content)); - if (contentType == default) contentType = ContentType.TextPlain; + if (contentType == default) + contentType = ContentType.TextPlain; return await SendToUserAsync(userId, RequestContent.Create(content), contentType, context: default).ConfigureAwait(false); } @@ -217,7 +222,8 @@ public virtual Response SendToUser(string userId, string content, ContentType co Argument.AssertNotNull(userId, nameof(userId)); Argument.AssertNotNull(content, nameof(content)); - if (contentType == default) contentType = ContentType.TextPlain; + if (contentType == default) + contentType = ContentType.TextPlain; return SendToUser(userId, RequestContent.Create(content), contentType, context: default); } @@ -234,7 +240,8 @@ public virtual async Task SendToConnectionAsync(string connectionId, s Argument.AssertNotNull(connectionId, nameof(connectionId)); Argument.AssertNotNull(content, nameof(content)); - if (contentType == default) contentType = ContentType.TextPlain; + if (contentType == default) + contentType = ContentType.TextPlain; return await SendToConnectionAsync(connectionId, RequestContent.Create(content), contentType, context: default).ConfigureAwait(false); } @@ -251,7 +258,8 @@ public virtual Response SendToConnection(string connectionId, string content, Co Argument.AssertNotNull(connectionId, nameof(connectionId)); Argument.AssertNotNull(content, nameof(content)); - if (contentType == default) contentType = ContentType.TextPlain; + if (contentType == default) + contentType = ContentType.TextPlain; return SendToConnection(connectionId, RequestContent.Create(content), contentType, context: default); } @@ -268,9 +276,10 @@ public virtual async Task SendToGroupAsync(string group, string conten Argument.AssertNotNull(group, nameof(group)); Argument.AssertNotNull(content, nameof(content)); - if (contentType == default) contentType = ContentType.TextPlain; + if (contentType == default) + contentType = ContentType.TextPlain; - return await SendToGroupAsync(group, RequestContent.Create(content), contentType, excluded : default, context: default).ConfigureAwait(false); + return await SendToGroupAsync(group, RequestContent.Create(content), contentType, excluded: default, context: default).ConfigureAwait(false); } /// @@ -285,7 +294,8 @@ public virtual Response SendToGroup(string group, string content, ContentType co Argument.AssertNotNull(group, nameof(group)); Argument.AssertNotNull(content, nameof(content)); - if (contentType == default) contentType = ContentType.TextPlain; + if (contentType == default) + contentType = ContentType.TextPlain; return SendToGroup(group, RequestContent.Create(content), contentType, excluded: default, context: default); } @@ -549,5 +559,63 @@ public virtual Response RemoveUserFromGroup(string group, string userId, Request throw; } } + + /// + /// Add filtered connections to multiple groups. + /// + /// A list of groups which target connections will be added into. + /// An OData filter which target connections satisfy. + /// The request context, which can override default behaviors of the client pipeline on a per-call basis. + /// A if successful. + public virtual Response AddConnectionsToGroups(IEnumerable groups, string filter, RequestContext context = null) + { + Argument.AssertNotNull(filter, nameof(filter)); + Argument.AssertNotNull(groups, nameof(groups)); + + return AddConnectionsToGroups(RequestContent.Create(new { filter = filter, groups = groups }), context); + } + + /// + /// Add filtered connections to multiple groups. + /// + /// A list of groups which target connections will be added into. + /// An OData filter which target connections satisfy. + /// The request context, which can override default behaviors of the client pipeline on a per-call basis. + /// A if successful. + public virtual async Task AddConnectionsToGroupsAsync(IEnumerable groups, string filter, RequestContext context = null) + { + Argument.AssertNotNull(filter, nameof(filter)); + Argument.AssertNotNull(groups, nameof(groups)); + + return await AddConnectionsToGroupsAsync(RequestContent.Create(new { filter = filter, groups = groups }), context).ConfigureAwait(false); + } + + /// + /// Remove filtered connections from multiple groups. + /// + /// A list of groups which target connections will be added into. + /// An OData filter which target connections satisfy. + /// The request context, which can override default behaviors of the client pipeline on a per-call basis. + /// A if successful. + public virtual Response RemoveConnectionsFromGroups(IEnumerable groups, string filter = null, RequestContext context = null) + { + Argument.AssertNotNull(groups, nameof(groups)); + + return RemoveConnectionsFromGroups(RequestContent.Create(new { filter = filter, groups = groups }), context); + } + + /// + /// Remove filtered connections from multiple groups. + /// + /// A list of groups which target connections will be added into. + /// An OData filter which target connections satisfy. + /// The request context, which can override default behaviors of the client pipeline on a per-call basis. + /// A if successful. + public virtual async Task RemoveConnectionsFromGroupsAsync(IEnumerable groups, string filter = null, RequestContext context = null) + { + Argument.AssertNotNull(groups, nameof(groups)); + + return await RemoveConnectionsFromGroupsAsync(RequestContent.Create(new { filter = filter, groups = groups }), context).ConfigureAwait(false); + } } } diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClient_helpers.cs b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClient_helpers.cs index 3c7fabeaa4eca..dcaef57f024fb 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClient_helpers.cs +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/WebPubSubServiceClient_helpers.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Text.Json; @@ -30,7 +31,7 @@ public partial class WebPubSubServiceClient internal static byte[] s_group = Encoding.UTF8.GetBytes("webpubsub.group"); /// - /// Creates a URI with authentication token. + /// Creates a URI with authentication token for the clients. /// /// UTC time when the token expires. /// User Id. @@ -38,12 +39,13 @@ public partial class WebPubSubServiceClient /// Roles that the connection with the generated token will have. /// #pragma warning disable AZC0015 // Unexpected client method return type. + [EditorBrowsable(EditorBrowsableState.Never)] public virtual Uri GetClientAccessUri(DateTimeOffset expiresAt, string userId, IEnumerable roles, CancellationToken cancellationToken) #pragma warning restore AZC0015 // Unexpected client method return type. - => GetClientAccessUriInternal(expiresAt, userId, roles, null, async: false, cancellationToken).EnsureCompleted(); + => GetClientAccessUriInternal(expiresAt, userId, roles, null, WebPubSubClientProtocol.Default, async: false, cancellationToken).EnsureCompleted(); /// - /// Creates a URI with authentication token. + /// Creates a URI with authentication token for the clients. /// /// UTC time when the token expires. /// User Id. @@ -52,12 +54,28 @@ public virtual Uri GetClientAccessUri(DateTimeOffset expiresAt, string userId, I /// Groups that the connection with the generated token will join when it connects. /// #pragma warning disable AZC0015 // Unexpected client method return type. - public virtual Uri GetClientAccessUri(DateTimeOffset expiresAt, string userId = default, IEnumerable roles = default, IEnumerable groups = default, CancellationToken cancellationToken = default) + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual Uri GetClientAccessUri(DateTimeOffset expiresAt, string userId, IEnumerable roles, IEnumerable groups, CancellationToken cancellationToken) #pragma warning restore AZC0015 // Unexpected client method return type. - => GetClientAccessUriInternal(expiresAt, userId, roles, groups, async: false, cancellationToken).EnsureCompleted(); + => GetClientAccessUriInternal(expiresAt, userId, roles, groups, WebPubSubClientProtocol.Default, async: false, cancellationToken).EnsureCompleted(); /// - /// Creates a URI with authentication token. + /// Creates a URI with authentication token for the clients. + /// + /// UTC time when the token expires. + /// User Id. + /// Cancellation token. + /// Roles that the connection with the generated token will have. + /// Groups that the connection with the generated token will join when it connects. + /// The client protocol. + /// +#pragma warning disable AZC0015 // Unexpected client method return type. + public virtual Uri GetClientAccessUri(DateTimeOffset expiresAt, string userId = default, IEnumerable roles = default, IEnumerable groups = default, WebPubSubClientProtocol clientProtocol = default, CancellationToken cancellationToken = default) +#pragma warning restore AZC0015 // Unexpected client method return type. + => GetClientAccessUriInternal(expiresAt, userId, roles, groups, clientProtocol, async: false, cancellationToken).EnsureCompleted(); + + /// + /// Creates a URI with authentication token for the clients. /// /// UTC time when the token expires. /// User Id. @@ -65,50 +83,91 @@ public virtual Uri GetClientAccessUri(DateTimeOffset expiresAt, string userId = /// Roles that the connection with the generated token will have. /// #pragma warning disable AZC0015 // Unexpected client method return type. + [EditorBrowsable(EditorBrowsableState.Never)] public virtual async Task GetClientAccessUriAsync(DateTimeOffset expiresAt, string userId, IEnumerable roles, CancellationToken cancellationToken) - => await GetClientAccessUriInternal(expiresAt, userId, roles, null, async: true, cancellationToken).ConfigureAwait(false); + => await GetClientAccessUriInternal(expiresAt, userId, roles, null, WebPubSubClientProtocol.Default, async: true, cancellationToken).ConfigureAwait(false); +#pragma warning restore AZC0015 // Unexpected client method return type. + + /// + /// Creates a URI with authentication token for the clients. + /// + /// UTC time when the token expires. + /// User Id. + /// Cancellation token. + /// Roles that the connection with the generated token will have. + /// Groups that the connection with the generated token will join when it connects. + /// +#pragma warning disable AZC0015 // Unexpected client method return type. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual async Task GetClientAccessUriAsync(DateTimeOffset expiresAt, string userId, IEnumerable roles, IEnumerable groups, CancellationToken cancellationToken) + => await GetClientAccessUriInternal(expiresAt, userId, roles, groups, WebPubSubClientProtocol.Default, async: true, cancellationToken).ConfigureAwait(false); #pragma warning restore AZC0015 // Unexpected client method return type. /// - /// Creates a URI with authentication token. + /// Creates a URI with authentication token for the clients.. /// /// UTC time when the token expires. /// User Id. /// Cancellation token. /// Roles that the connection with the generated token will have. /// Groups that the connection with the generated token will join when it connects. + /// The client protocol. + /// +#pragma warning disable AZC0015 // Unexpected client method return type. + public virtual async Task GetClientAccessUriAsync(DateTimeOffset expiresAt, string userId = default, IEnumerable roles = default, IEnumerable groups = default, WebPubSubClientProtocol clientProtocol = default, CancellationToken cancellationToken = default) + => await GetClientAccessUriInternal(expiresAt, userId, roles, groups, clientProtocol, async: true, cancellationToken).ConfigureAwait(false); +#pragma warning restore AZC0015 // Unexpected client method return type. + + /// + /// Creates a URI with authentication token for the clients. + /// + /// Defaults to one hour, if not specified. Must be greater or equal zero. + /// User Id. + /// Roles that the connection with the generated token will have. + /// Cancellation token. /// #pragma warning disable AZC0015 // Unexpected client method return type. - public virtual async Task GetClientAccessUriAsync(DateTimeOffset expiresAt, string userId = default, IEnumerable roles = default, IEnumerable groups = default, CancellationToken cancellationToken = default) - => await GetClientAccessUriInternal(expiresAt, userId, roles, groups, async: true, cancellationToken).ConfigureAwait(false); + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual Uri GetClientAccessUri( + TimeSpan expiresAfter, + string userId, + IEnumerable roles, + CancellationToken cancellationToken) #pragma warning restore AZC0015 // Unexpected client method return type. + { + return GetClientAccessUriInternal(expiresAfter, userId, roles, null, WebPubSubClientProtocol.Default, false, cancellationToken).EnsureCompleted(); + } /// - /// Creates a URI with authentication token. + /// Creates a URI with authentication token for the clients. /// /// Defaults to one hour, if not specified. Must be greater or equal zero. /// User Id. /// Roles that the connection with the generated token will have. + /// Groups that the connection with the generated token will join when it connects. /// Cancellation token. /// #pragma warning disable AZC0015 // Unexpected client method return type. + [EditorBrowsable(EditorBrowsableState.Never)] public virtual Uri GetClientAccessUri( TimeSpan expiresAfter, string userId, IEnumerable roles, + IEnumerable groups, CancellationToken cancellationToken) #pragma warning restore AZC0015 // Unexpected client method return type. { - return GetClientAccessUriInternal(expiresAfter, userId, roles, null, false, cancellationToken).EnsureCompleted(); + return GetClientAccessUriInternal(expiresAfter, userId, roles, groups, WebPubSubClientProtocol.Default, false, cancellationToken).EnsureCompleted(); } /// - /// Creates a URI with authentication token. + /// Creates a URI with authentication token for the clients. /// /// Defaults to one hour, if not specified. Must be greater or equal zero. /// User Id. /// Roles that the connection with the generated token will have. /// Groups that the connection with the generated token will join when it connects. + /// The client protocol. /// Cancellation token. /// #pragma warning disable AZC0015 // Unexpected client method return type. @@ -117,38 +176,63 @@ public virtual Uri GetClientAccessUri( string userId = default, IEnumerable roles = default, IEnumerable groups = default, + WebPubSubClientProtocol clientProtocol = default, CancellationToken cancellationToken = default) #pragma warning restore AZC0015 // Unexpected client method return type. { - return GetClientAccessUriInternal(expiresAfter, userId, roles, groups, false, cancellationToken).EnsureCompleted(); + return GetClientAccessUriInternal(expiresAfter, userId, roles, groups, clientProtocol, false, cancellationToken).EnsureCompleted(); + } + + /// + /// Creates a URI with authentication token for the clients. + /// + /// Defaults to one hour, if not specified. Must be greater or equal zero. + /// User Id. + /// Roles that the connection with the generated token will have. + /// Cancellation token. + /// +#pragma warning disable AZC0015 // Unexpected client method return type. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual async Task GetClientAccessUriAsync( + TimeSpan expiresAfter, + string userId, + IEnumerable roles, + CancellationToken cancellationToken) +#pragma warning restore AZC0015 // Unexpected client method return type. + { + return await GetClientAccessUriInternal(expiresAfter, userId, roles, null, WebPubSubClientProtocol.Default, true, cancellationToken).ConfigureAwait(false); } /// - /// Creates a URI with authentication token. + /// Creates a URI with authentication token for the clients. /// /// Defaults to one hour, if not specified. Must be greater or equal zero. /// User Id. /// Roles that the connection with the generated token will have. + /// Groups that the connection with the generated token will join when it connects. /// Cancellation token. /// #pragma warning disable AZC0015 // Unexpected client method return type. + [EditorBrowsable(EditorBrowsableState.Never)] public virtual async Task GetClientAccessUriAsync( TimeSpan expiresAfter, string userId, IEnumerable roles, + IEnumerable groups, CancellationToken cancellationToken) #pragma warning restore AZC0015 // Unexpected client method return type. { - return await GetClientAccessUriInternal(expiresAfter, userId, roles, null, true, cancellationToken).ConfigureAwait(false); + return await GetClientAccessUriInternal(expiresAfter, userId, roles, groups, WebPubSubClientProtocol.Default, true, cancellationToken).ConfigureAwait(false); } /// - /// Creates a URI with authentication token. + /// Creates a URI with authentication token for the clients. /// /// Defaults to one hour, if not specified. Must be greater or equal zero. /// User Id. /// Roles that the connection with the generated token will have. /// Groups that the connection with the generated token will join when it connects. + /// The client protocol. /// Cancellation token. /// #pragma warning disable AZC0015 // Unexpected client method return type. @@ -157,10 +241,11 @@ public virtual async Task GetClientAccessUriAsync( string userId = default, IEnumerable roles = default, IEnumerable groups = default, + WebPubSubClientProtocol clientProtocol = default, CancellationToken cancellationToken = default) #pragma warning restore AZC0015 // Unexpected client method return type. { - return await GetClientAccessUriInternal(expiresAfter, userId, roles, groups, true, cancellationToken).ConfigureAwait(false); + return await GetClientAccessUriInternal(expiresAfter, userId, roles, groups, clientProtocol, true, cancellationToken).ConfigureAwait(false); } internal static int GetMinutesToExpire(TimeSpan expiresAfter) => Math.Max((int)expiresAfter.TotalMinutes, 1); @@ -168,85 +253,28 @@ public virtual async Task GetClientAccessUriAsync( internal static int GetMinutesToExpire(DateTimeOffset expiresAt) => Math.Max((int)expiresAt.Subtract(DateTimeOffset.UtcNow).TotalMinutes, 1); private async Task GetClientAccessUriInternal( - DateTimeOffset expiresAt, - string userId = default, - IEnumerable roles = default, - IEnumerable groups = default, - bool async = true, - CancellationToken cancellationToken = default) + DateTimeOffset expiresAt, + string userId = default, + IEnumerable roles = default, + IEnumerable groups = default, + WebPubSubClientProtocol clientProtocol = default, + bool async = true, + CancellationToken cancellationToken = default) { - string token; - - if (_tokenCredential != null) - { - RequestContext context = new() { CancellationToken = cancellationToken }; - - var minutesToExpire = GetMinutesToExpire(expiresAt); - - Response clientTokenResponse = async ? - await GenerateClientTokenImplAsync(userId, roles, minutesToExpire, groups, context).ConfigureAwait(false) : - GenerateClientTokenImpl(userId, roles, minutesToExpire, null, context); - using var jsonDocument = JsonDocument.Parse(clientTokenResponse.Content); - token = jsonDocument.RootElement.GetProperty(ClientTokenResponseTokenPropertyName).GetString(); - } - else if (_credential != null) - { - token = GenerateTokenFromAzureKeyCredential(expiresAt, userId, roles, groups); - } - else - { - throw new InvalidOperationException($"{nameof(WebPubSubServiceClient)} must be constructed with either a {typeof(TokenCredential)} or {typeof(AzureKeyCredential)} to generate client access URIs."); - } - - UriBuilder clientEndpoint = new(Endpoint) - { - Scheme = Endpoint.Scheme == "http" ? "ws" : "wss" - }; - - return new Uri($"{clientEndpoint}client/hubs/{_hub}?access_token={token}"); + var token = await GetClientAccessTokenCore(expiresAt, userId, roles, groups, clientProtocol, async, cancellationToken: cancellationToken).ConfigureAwait(false); + return GetClientAccessUriInternal(token, clientProtocol); } - private async Task GetClientAccessUriInternal( + private Task GetClientAccessUriInternal( TimeSpan expireAfter, string userId = default, IEnumerable roles = default, IEnumerable groups = default, + WebPubSubClientProtocol clientProtocol = default, bool async = true, CancellationToken cancellationToken = default) { - string token; - - if (_tokenCredential != null) - { - RequestContext context = new() { CancellationToken = cancellationToken }; - - var minutesToExpire = GetMinutesToExpire(expireAfter); - - Response clientTokenResponse = async ? - await GenerateClientTokenImplAsync(userId, roles, minutesToExpire, groups, context).ConfigureAwait(false) : - GenerateClientTokenImpl(userId, roles, minutesToExpire, null, context); - using var jsonDocument = JsonDocument.Parse(clientTokenResponse.Content); - token = jsonDocument.RootElement.GetProperty(ClientTokenResponseTokenPropertyName).GetString(); - } - else if (_credential != null) - { - if (expireAfter == default) - { - expireAfter = TimeSpan.FromHours(1); - } - token = GenerateTokenFromAzureKeyCredential(DateTimeOffset.UtcNow.Add(expireAfter), userId, roles, groups); - } - else - { - throw new InvalidOperationException($"{nameof(WebPubSubServiceClient)} must be constructed with either a {typeof(TokenCredential)} or {typeof(AzureKeyCredential)} to generate client access URIs."); - } - - UriBuilder clientEndpoint = new(Endpoint) - { - Scheme = Endpoint.Scheme == "http" ? "ws" : "wss" - }; - - return new Uri($"{clientEndpoint}client/hubs/{_hub}?access_token={token}"); + return GetClientAccessUriInternal(expireAfter == default ? DateTimeOffset.UtcNow.Add(TimeSpan.FromHours(1)) : DateTimeOffset.UtcNow.Add(expireAfter), userId, roles, groups, clientProtocol, async, cancellationToken); } /// @@ -321,7 +349,7 @@ internal static string PermissionToString(WebPubSubPermission permission) } } - private string GenerateTokenFromAzureKeyCredential(DateTimeOffset expiresAt, string userId = default, IEnumerable roles = default, IEnumerable groups = default) + private string GenerateTokenFromAzureKeyCredential(DateTimeOffset expiresAt, WebPubSubClientProtocol clientProtocol, string userId = default, IEnumerable roles = default, IEnumerable groups = default) { var keyBytes = Encoding.UTF8.GetBytes(_credential.Key); @@ -333,7 +361,7 @@ private string GenerateTokenFromAzureKeyCredential(DateTimeOffset expiresAt, str { endpoint += "/"; } - var audience = $"{endpoint}client/hubs/{_hub}"; + var audience = $"{endpoint}{GetRelativeClientEndpoint(clientProtocol)}"; if (userId != default) { @@ -354,5 +382,63 @@ private string GenerateTokenFromAzureKeyCredential(DateTimeOffset expiresAt, str return jwt.BuildString(); } + + private string GetRelativeClientEndpoint(WebPubSubClientProtocol clientProtocol) => clientProtocol switch + { + WebPubSubClientProtocol.Default => $"client/hubs/{_hub}", + WebPubSubClientProtocol.Mqtt => $"clients/mqtt/hubs/{_hub}", + _ => throw new ArgumentOutOfRangeException(nameof(clientProtocol)) + }; + + private async Task GetClientAccessTokenCore( + DateTimeOffset expiresAt, + string userId, + IEnumerable roles, + IEnumerable groups, + WebPubSubClientProtocol clientAccess, + bool async, + CancellationToken cancellationToken = default) + { + if (clientAccess == WebPubSubClientProtocol.Mqtt && _apiVersionEnum < WebPubSubServiceClientOptions.ServiceVersion.V2024_01_01) + { + throw new NotSupportedException($"Generating a client access URI for MQTT is only supported in API version {WebPubSubServiceClientOptions.ServiceVersion.V2024_01_01.ToVersionString()} or later. You are currently using API version {_apiVersion}."); + } + var clientEndpointString = clientAccess switch + { + WebPubSubClientProtocol.Default => "default", + WebPubSubClientProtocol.Mqtt => "mqtt", + _ => throw new ArgumentOutOfRangeException(nameof(clientAccess)) + }; + if (_tokenCredential != null) + { + RequestContext context = new() { CancellationToken = cancellationToken }; + + var minutesToExpire = GetMinutesToExpire(expiresAt); + + Response clientTokenResponse = async ? + await GenerateClientTokenImplAsync(userId, roles, minutesToExpire, groups, clientEndpointString, context).ConfigureAwait(false) : + GenerateClientTokenImpl(userId, roles, minutesToExpire, null, clientEndpointString, context); + using var jsonDocument = JsonDocument.Parse(clientTokenResponse.Content); + return jsonDocument.RootElement.GetProperty(ClientTokenResponseTokenPropertyName).GetString(); + } + else if (_credential != null) + { + return GenerateTokenFromAzureKeyCredential(expiresAt, clientAccess, userId, roles, groups); + } + else + { + throw new InvalidOperationException($"{nameof(WebPubSubServiceClient)} must be constructed with either a {typeof(TokenCredential)} or {typeof(AzureKeyCredential)} to generate client access URIs."); + } + } + + private Uri GetClientAccessUriInternal(string token, WebPubSubClientProtocol endpointType) + { + UriBuilder clientEndpoint = new(Endpoint) + { + Scheme = Endpoint.Scheme == "http" ? "ws" : "wss" + }; + + return new Uri($"{clientEndpoint}{GetRelativeClientEndpoint(endpointType)}?access_token={token}"); + } } } diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/autorest.md b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/autorest.md index 3a4fe81c8418f..576a2507a5a43 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/src/autorest.md +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/src/autorest.md @@ -9,7 +9,8 @@ Run `dotnet build /t:GenerateCode` to generate code. ``` yaml title: WebPubSubServiceClient input-file: -- https://github.com/Azure/azure-rest-api-specs/blob/1735a92bdc79b446385a36ba063ea5235680709f/specification/webpubsub/data-plane/WebPubSub/stable/2022-11-01/webpubsub.json +- https://github.com/Azure/azure-rest-api-specs/blob/356aa5174e8eec6ed904bf5ff104595aec8c0411/specification/webpubsub/data-plane/WebPubSub/stable/2024-01-01/webpubsub.json + credential-types: AzureKeyCredential credential-header-name: Ocp-Apim-Subscription-Key keep-non-overloadable-protocol-signature: true @@ -23,6 +24,67 @@ directive: transform: $.modelAsString = false; ``` +### Restore the "host" parameter to be a string instead of a URL to avoid breaking change +``` yaml +directive: +- from: swagger-document + where: $.x-ms-parameterized-host.parameters[0].format + transform: return "string"; +``` + +### Remove "messageTtlSeconds" parameter from all operations as the generated sample uses a invalid value for it. +``` yaml +directive: +- where-operation: WebPubSub_SendToConnection + remove-parameter: + debug: true + in: query + name: messageTtlSeconds +- where-operation: WebPubSub_SendToGroup + remove-parameter: + debug: true + in: query + name: messageTtlSeconds +- where-operation: WebPubSub_SendToUser + remove-parameter: + debug: true + in: query + name: messageTtlSeconds +- where-operation: WebPubSub_SendToAll + remove-parameter: + debug: true + in: query + name: messageTtlSeconds +``` + +### AddConnectionsToGroupsAsyncImpl +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/:addToGroups"].post.operationId + transform: return "WebPubSubService_AddConnectionsToGroups"; +- from: swagger-document + where: $.paths["/api/hubs/{hub}/:addToGroups"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +- from: swagger-document + where: $.paths["/api/hubs/{hub}/:addToGroups"].post + transform: $["x-accessibility"] = "internal" +``` + +### RemoveConnectionsFromGroups +``` yaml +directive: +- from: swagger-document + where: $.paths["/api/hubs/{hub}/:removeFromGroups"].post.operationId + transform: return "WebPubSubService_RemoveConnectionsFromGroups"; +- from: swagger-document + where: $.paths["/api/hubs/{hub}/:removeFromGroups"].post.parameters["0"] + transform: $["x-ms-parameter-location"] = "client" +- from: swagger-document + where: $.paths["/api/hubs/{hub}/:removeFromGroups"].post + transform: $["x-accessibility"] = "internal" +``` + ### GenerateClientTokenImpl ``` yaml directive: diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/tests/WebPubSubGenerateUriTests.cs b/sdk/webpubsub/Azure.Messaging.WebPubSub/tests/WebPubSubGenerateUriTests.cs index ae152b39c3bd5..ad7a43a3fa3a1 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/tests/WebPubSubGenerateUriTests.cs +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/tests/WebPubSubGenerateUriTests.cs @@ -2,11 +2,15 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Threading; +using System.Threading.Tasks; using System.Web; - +using Azure.Core; +using Azure.Identity; +using Moq; using NUnit.Framework; namespace Azure.Messaging.WebPubSub.Tests @@ -18,6 +22,60 @@ public class WebPubSubGenerateUriTests private static readonly JwtSecurityTokenHandler s_jwtTokenHandler = new(); + [TestCase(WebPubSubClientProtocol.Default, "/client")] + [TestCase(WebPubSubClientProtocol.Mqtt, "/clients/mqtt")] + public async Task GetClientAccessUri_AccessKey_Test(WebPubSubClientProtocol clientType, string clientUriPrefix) + { + var serviceClient = new WebPubSubServiceClient(string.Format("Endpoint=http://localhost;Port=8080;AccessKey={0};Version=1.0;", FakeAccessKey), "hub"); + var expectedUriPrefix = $"ws://localhost:8080{clientUriPrefix}/hubs/hub?access_token="; + // Synchronize + Assert.True(serviceClient.GetClientAccessUri(TimeSpan.FromMinutes(1), default, default, default, clientType, default).ToString().StartsWith(expectedUriPrefix)); + Assert.True(serviceClient.GetClientAccessUri(DateTimeOffset.UtcNow.AddMinutes(1), default, default, default, clientType, default).ToString().StartsWith(expectedUriPrefix)); + // Asynchronize + Assert.True((await serviceClient.GetClientAccessUriAsync(TimeSpan.FromMinutes(1), default, default, default, clientType, default)).ToString().StartsWith(expectedUriPrefix)); + Assert.True((await serviceClient.GetClientAccessUriAsync(DateTimeOffset.Now, default, default, default, clientType, default)).ToString().StartsWith(expectedUriPrefix)); + } + + [TestCase(WebPubSubClientProtocol.Default, "/client", "default")] + [TestCase(WebPubSubClientProtocol.Mqtt, "/clients/mqtt", "mqtt")] + public async Task GetClientAccessUri_MicrosoftEntraId_DefaultClient_Test(WebPubSubClientProtocol clientType, string clientUriPrefix, string clientTypeString) + { + var serviceClient = new WebPubSubServiceSubClass(new Uri("https://localhost"), "hub", new DefaultAzureCredential()); + var expectedUri = new Uri($"wss://localhost{clientUriPrefix}/hubs/hub?access_token=fakeToken"); + Assert.AreEqual(expectedUri, serviceClient.GetClientAccessUri(TimeSpan.FromMinutes(1), default, default, default, clientType, default)); + Assert.AreEqual(expectedUri, serviceClient.GetClientAccessUri(DateTime.UtcNow, default, default, default, clientType, default)); + Assert.AreEqual(expectedUri, await serviceClient.GetClientAccessUriAsync(TimeSpan.FromMinutes(1), default, default, default, clientType, default)); + Assert.AreEqual(expectedUri, await serviceClient.GetClientAccessUriAsync(DateTime.UtcNow, default, default, default, clientType, default)); + for (var i = 0; i < 4; i++) + { + // Validate the "clientType" parameter passed to the GenerateClientTokenImpl(Async) method + Assert.True(clientTypeString == serviceClient.InvocationParameters[i][4].ToString()); + } + } + + /// + /// A subclass of WebPubSubServiceClient to mock method GenerateClientTokenImpl(Async) + /// + private class WebPubSubServiceSubClass : WebPubSubServiceClient + { + public WebPubSubServiceSubClass(Uri endpoint, string hub, TokenCredential credential) + : base(endpoint, hub, credential) + { + } + public List InvocationParameters { get; } = new(); + internal override Task GenerateClientTokenImplAsync(string userId = null, IEnumerable role = null, int? minutesToExpire = null, IEnumerable group = null, string clientType = null, RequestContext context = null) + { + InvocationParameters.Add(new object[] { userId, role, minutesToExpire, group, clientType, context }); + return Task.FromResult(Mock.Of(r => r.Content == new BinaryData("{\"token\":\"fakeToken\"}"))); + } + + internal override Response GenerateClientTokenImpl(string userId = null, IEnumerable role = null, int? minutesToExpire = null, IEnumerable group = null, string clientType = null, RequestContext context = null) + { + InvocationParameters.Add(new object[] { userId, role, minutesToExpire, group, clientType, context }); + return Mock.Of(r => r.Content == new BinaryData("{\"token\":\"fakeToken\"}")); + } + } + [TestCase(0, 60)] [TestCase(1, 1)] [TestCase(2, 2)] @@ -29,7 +87,7 @@ public void AccessKeyExpiresAfterTests(int minutesToExpire, int expectedMinutesA var utcnow = DateTimeOffset.UtcNow; var expiresAfter = TimeSpan.FromMinutes(minutesToExpire); - Uri uri = serviceClient.GetClientAccessUri(expiresAfter, "foo", null ); + Uri uri = serviceClient.GetClientAccessUri(expiresAfter, "foo", null); var token = HttpUtility.ParseQueryString(uri.Query).Get("access_token"); Assert.NotNull(token); var jwt = s_jwtTokenHandler.ReadJwtToken(token); @@ -48,7 +106,7 @@ public void AccessKeyExpireAtTests(int minutesToExpire) var serviceClient = new WebPubSubServiceClient(string.Format("Endpoint=http://localhost;Port=8080;AccessKey={0};Version=1.0;", FakeAccessKey), "hub"); var expireAt = DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(minutesToExpire)); - Uri uri = serviceClient.GetClientAccessUri(expireAt, "foo", null ); + Uri uri = serviceClient.GetClientAccessUri(expireAt, "foo", null); var token = HttpUtility.ParseQueryString(uri.Query).Get("access_token"); Assert.NotNull(token); var jwt = s_jwtTokenHandler.ReadJwtToken(token); @@ -94,7 +152,7 @@ public void CreateGenerateClientTokenImplRequestTest() var source = new CancellationTokenSource(); RequestContext context = new() { CancellationToken = source.Token }; var expectRoles = new[] { "a", "b" }; - var request = client.CreateGenerateClientTokenImplRequest("foo", new[] {"a", "b"}, 1, null, context); + var request = client.CreateGenerateClientTokenImplRequest("foo", new[] { "a", "b" }, 1, null, "default", context); var url = request.Request.Uri.ToString(); var queryString = url.Substring(url.IndexOf('?')); diff --git a/sdk/webpubsub/Azure.Messaging.WebPubSub/tests/WebPubSubTestEnvironment.cs b/sdk/webpubsub/Azure.Messaging.WebPubSub/tests/WebPubSubTestEnvironment.cs index 0a2362121269e..fbaa0c7cbf35c 100644 --- a/sdk/webpubsub/Azure.Messaging.WebPubSub/tests/WebPubSubTestEnvironment.cs +++ b/sdk/webpubsub/Azure.Messaging.WebPubSub/tests/WebPubSubTestEnvironment.cs @@ -7,6 +7,6 @@ namespace Azure.Rest.WebPubSub.Tests { public class WebPubSubTestEnvironment : TestEnvironment { - public string ConnectionString => GetRecordedVariable("WEBPUBSUB_CONNECTIONSTRING", options => options.HasSecretConnectionStringParameter("secret", SanitizedValue.Base64)); + public string ConnectionString => GetRecordedVariable("WEBPUBSUB_CONNECTIONSTRING", options => options.HasSecretConnectionStringParameter("accessKey", SanitizedValue.Base64)); } }