diff --git a/Source/ZoomNet.IntegrationTests/Tests/Accounts.cs b/Source/ZoomNet.IntegrationTests/Tests/Accounts.cs index 3e6240ed..4328b1b2 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Accounts.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Accounts.cs @@ -14,20 +14,23 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie await log.WriteLineAsync("\n***** ACCOUNTS *****\n").ConfigureAwait(false); - // GET ALL THE ACCOUNTS - var paginatedAccounts = await client.Accounts.GetAllAsync(100, null, cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync($"There are {paginatedAccounts.TotalRecords} sub accounts under the main account").ConfigureAwait(false); - - // GET SETTINGS - if (paginatedAccounts.Records.Any()) + if (client.HasPermission("account:read:list_sub_accounts:master")) { - var accountId = paginatedAccounts.Records.First().Id; + // GET ALL THE ACCOUNTS + var paginatedAccounts = await client.Accounts.GetAllAsync(100, null, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync($"There are {paginatedAccounts.TotalRecords} sub accounts under the main account").ConfigureAwait(false); + + // GET SETTINGS + if (paginatedAccounts.Records.Any()) + { + var accountId = paginatedAccounts.Records.First().Id; - var meetingAuthenticationSettings = await client.Accounts.GetMeetingAuthenticationSettingsAsync(accountId, cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync("Meeting authentication settings retrieved").ConfigureAwait(false); + var meetingAuthenticationSettings = await client.Accounts.GetMeetingAuthenticationSettingsAsync(accountId, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync("Meeting authentication settings retrieved").ConfigureAwait(false); - var recordingAuthenticationSettings = await client.Accounts.GetRecordingAuthenticationSettingsAsync(accountId, cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync("Recording authentication settings retrieved").ConfigureAwait(false); + var recordingAuthenticationSettings = await client.Accounts.GetRecordingAuthenticationSettingsAsync(accountId, cancellationToken).ConfigureAwait(false); + await log.WriteLineAsync("Recording authentication settings retrieved").ConfigureAwait(false); + } } } } diff --git a/Source/ZoomNet/Extensions/Public.cs b/Source/ZoomNet/Extensions/Public.cs index 60faab06..6991b196 100644 --- a/Source/ZoomNet/Extensions/Public.cs +++ b/Source/ZoomNet/Extensions/Public.cs @@ -368,5 +368,22 @@ public static async Task AddUserToGroupAsync(this IGroups groupsResource // We added a single member to a group therefore the array returned from the Zoom API contains a single element return result.Single(); } + + /// + /// Determines if the specified scope has been granted. + /// + /// The ZoomNet client. + /// The name of the scope. + /// True if the scope has been granted, False otherwise. + /// + /// The concept of "scopes" only applies to OAuth connections. + /// Therefore an exeption will be thrown if you invoke this method while using + /// a JWT connection (you shouldn't be using JWT in the first place since this + /// type of connection has been deprecated in the Zoom API since September 2023). + /// + public static bool HasPermission(this IZoomClient client, string scope) + { + return client.HasPermissions(new[] { scope }); + } } } diff --git a/Source/ZoomNet/IZoomClient.cs b/Source/ZoomNet/IZoomClient.cs index d38f20cc..4bba5eb2 100644 --- a/Source/ZoomNet/IZoomClient.cs +++ b/Source/ZoomNet/IZoomClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using ZoomNet.Resources; namespace ZoomNet @@ -105,5 +106,18 @@ public interface IZoomClient /// Gets the resource which allows you to manage webinars. /// IWebinars Webinars { get; } + + /// + /// Determines if the specified scopes have been granted. + /// + /// The name of the scopes. + /// True if all the scopes have been granted, False otherwise. + /// + /// The concept of "scopes" only applies to OAuth connections. + /// Therefore an exeption will be thrown if you invoke this method while using + /// a JWT connection (you shouldn't be using JWT in the first place since this + /// type of connection has been deprecated in the Zoom API since September 2023). + /// + bool HasPermissions(IEnumerable scopes); } } diff --git a/Source/ZoomNet/OAuthConnectionInfo.cs b/Source/ZoomNet/OAuthConnectionInfo.cs index e3a07585..cd95b81a 100644 --- a/Source/ZoomNet/OAuthConnectionInfo.cs +++ b/Source/ZoomNet/OAuthConnectionInfo.cs @@ -53,9 +53,9 @@ public class OAuthConnectionInfo : IConnectionInfo public string AccessToken { get; internal set; } /// - /// Gets the token scope. + /// Gets the scopes. /// - public IReadOnlyDictionary TokenScope { get; internal set; } + public IReadOnlyList Scopes { get; internal set; } /// /// Gets the token expiration time. diff --git a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs index 189ba8c7..1ab05467 100644 --- a/Source/ZoomNet/Utilities/OAuthTokenHandler.cs +++ b/Source/ZoomNet/Utilities/OAuthTokenHandler.cs @@ -121,16 +121,7 @@ public string RefreshTokenIfNecessary(bool forceRefresh) _connectionInfo.RefreshToken = jsonResponse.GetPropertyValue("refresh_token", string.Empty); _connectionInfo.AccessToken = jsonResponse.GetPropertyValue("access_token", string.Empty); _connectionInfo.TokenExpiration = requestTime.AddSeconds(jsonResponse.GetPropertyValue("expires_in", 60 * 60)); - _connectionInfo.TokenScope = new ReadOnlyDictionary( - jsonResponse.GetPropertyValue("scope", string.Empty) - .Split(' ') - .Select(x => x.Split(new[] { ':' }, 2)) - .Select(x => new KeyValuePair(x[0], x.Skip(1).ToArray())) - .GroupBy(x => x.Key) - .OrderBy(x => x.Key) - .ToDictionary( - x => x.Key, - x => x.SelectMany(c => c.Value).OrderBy(c => c).ToArray())); + _connectionInfo.Scopes = new ReadOnlyCollection(jsonResponse.GetPropertyValue("scope", string.Empty).Split(' ').OrderBy(x => x).ToList()); // Please note that Server-to-Server OAuth does not use the refresh token. // Therefore change the grant type to 'RefreshToken' only when the response includes a refresh token. diff --git a/Source/ZoomNet/ZoomClient.cs b/Source/ZoomNet/ZoomClient.cs index f91d70ff..3f8812d6 100644 --- a/Source/ZoomNet/ZoomClient.cs +++ b/Source/ZoomNet/ZoomClient.cs @@ -3,6 +3,8 @@ using Pathoschild.Http.Client; using Pathoschild.Http.Client.Extensibility; using System; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; @@ -244,6 +246,25 @@ private ZoomClient(IConnectionInfo connectionInfo, HttpClient httpClient, bool d #region PUBLIC METHODS + /// + public bool HasPermissions(IEnumerable scopes) + { + var tokenHandler = _fluentClient.Filters.OfType().SingleOrDefault(); + if (tokenHandler == null) throw new Exception("The concept of scopes only applies when using an OAuth connection."); + + // Ensure the token (and by extension the scopes) is not expired + tokenHandler.RefreshTokenIfNecessary(false); + + // The list of scopes can be empty if a previously issued token was specified when the OAuthConnectionInfo was instantiated. + // I am not aware of any way to fetch the list of scopes which would enable me to populate the list of scopes in the OAuthConnectionInfo. + // Therefore in this scenario the only workaround I can think of is to force the token to be refreshed. + var oAuthConnectionInfo = (OAuthConnectionInfo)tokenHandler.ConnectionInfo; + if (oAuthConnectionInfo.Scopes == null) tokenHandler.RefreshTokenIfNecessary(true); // Force the token to be refreshed wich will have the side-effect of populating the '.Scopes' + + var missingScopes = scopes.Except(((OAuthConnectionInfo)tokenHandler.ConnectionInfo).Scopes).ToArray(); + return !missingScopes.Any(); + } + /// public void Dispose() {