From 653338377118c4b3feba2286da579099f85be19b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 10 May 2018 18:09:14 +0200 Subject: [PATCH] Introduce OpenIddictScopeManager.FindByResourceAsync() to allow retrieving all the scopes associated with a given resource --- .../Managers/IOpenIddictScopeManager.cs | 11 +++++ .../Stores/IOpenIddictScopeStore.cs | 11 +++++ .../Managers/OpenIddictScopeManager.cs | 48 +++++++++++++++++++ .../Stores/OpenIddictScopeStore.cs | 46 ++++++++++++++++++ .../Stores/OpenIddictApplicationStore.cs | 9 ++-- .../Stores/OpenIddictScopeStore.cs | 44 ++++++++++++++++- 6 files changed, 165 insertions(+), 4 deletions(-) diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs index fbecbaa4..08d21856 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs @@ -103,6 +103,17 @@ namespace OpenIddict.Abstractions /// Task> FindByNamesAsync(ImmutableArray names, CancellationToken cancellationToken = default); + /// + /// Retrieves all the scopes that contain the specified resource. + /// + /// The resource associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified resource. + /// + Task> FindByResourceAsync([NotNull] string resource, CancellationToken cancellationToken = default); + /// /// Executes the specified query and returns the first element. /// diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs index 899669cb..fd204f66 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictScopeStore.cs @@ -95,6 +95,17 @@ namespace OpenIddict.Abstractions /// Task> FindByNamesAsync(ImmutableArray names, CancellationToken cancellationToken); + /// + /// Retrieves all the scopes that contain the specified resource. + /// + /// The resource associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified resource. + /// + Task> FindByResourceAsync([NotNull] string resource, CancellationToken cancellationToken); + /// /// Executes the specified query and returns the first element. /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs index 8c76eb93..03504067 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -241,6 +241,51 @@ namespace OpenIddict.Core builder.ToImmutable(); } + /// + /// Retrieves all the scopes that contain the specified resource. + /// + /// The resource associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified resource. + /// + public virtual async Task> FindByResourceAsync( + [NotNull] string resource, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); + } + + // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. + // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation + // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. + + var scopes = await Store.FindByResourceAsync(resource, cancellationToken); + if (scopes.IsEmpty) + { + return ImmutableArray.Create(); + } + + var builder = ImmutableArray.CreateBuilder(scopes.Length); + + foreach (var scope in scopes) + { + foreach (var value in await Store.GetResourcesAsync(scope, cancellationToken)) + { + if (string.Equals(value, resource, StringComparison.Ordinal)) + { + builder.Add(scope); + } + } + } + + return builder.Count == builder.Capacity ? + builder.MoveToImmutable() : + builder.ToImmutable(); + } + /// /// Executes the specified query and returns the first element. /// @@ -650,6 +695,9 @@ namespace OpenIddict.Core async Task> IOpenIddictScopeManager.FindByNamesAsync(ImmutableArray names, CancellationToken cancellationToken) => (await FindByNamesAsync(names, cancellationToken)).CastArray(); + async Task> IOpenIddictScopeManager.FindByResourceAsync(string resource, CancellationToken cancellationToken) + => (await FindByResourceAsync(resource, cancellationToken)).CastArray(); + Task IOpenIddictScopeManager.GetAsync(Func, IQueryable> query, CancellationToken cancellationToken) => GetAsync(query, cancellationToken); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs index aabaf9b0..2cbe2145 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs @@ -227,6 +227,52 @@ namespace OpenIddict.EntityFrameworkCore return ImmutableArray.CreateRange(await query(Context, names).ToListAsync(cancellationToken)); } + /// + /// Retrieves all the scopes that contain the specified resource. + /// + /// The resource associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified resource. + /// + public override async Task> FindByResourceAsync( + [NotNull] string resource, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); + } + + // To optimize the efficiency of the query a bit, only scopes whose stringified + // Resources column contains the specified resource are returned. Once the scopes + // are retrieved, a second pass is made to ensure only valid elements are returned. + // Implementers that use this method in a hot path may want to override this method + // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. + var query = Cache.GetOrCreate("45bae754-72fc-422c-b3a2-90867600a029", entry => + { + entry.SetPriority(CacheItemPriority.NeverRemove); + + return EF.CompileAsyncQuery((TContext context, string value) => + from scope in context.Set().AsTracking() + where scope.Resources.Contains(value) + select scope); + }); + + var builder = ImmutableArray.CreateBuilder(); + + foreach (var scope in await query(Context, resource).ToListAsync(cancellationToken)) + { + var resources = await GetResourcesAsync(scope, cancellationToken); + if (resources.Contains(resource, StringComparer.Ordinal)) + { + builder.Add(scope); + } + } + + return builder.ToImmutable(); + } + /// /// Executes the specified query and returns the first element. /// diff --git a/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs index bb66cbe6..c1cba8bc 100644 --- a/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs @@ -392,7 +392,8 @@ namespace OpenIddict.Stores // Note: parsing the stringified permissions is an expensive operation. // To mitigate that, the resulting array is stored in the memory cache. - var permissions = Cache.GetOrCreate("0347e0aa-3a26-410a-97e8-a83bdeb21a1f", entry => + var key = string.Concat("0347e0aa-3a26-410a-97e8-a83bdeb21a1f", "\x1e", application.Permissions); + var permissions = Cache.GetOrCreate(key, entry => { entry.SetPriority(CacheItemPriority.High) .SetSlidingExpiration(TimeSpan.FromMinutes(1)); @@ -428,7 +429,8 @@ namespace OpenIddict.Stores // Note: parsing the stringified addresses is an expensive operation. // To mitigate that, the resulting array is stored in the memory cache. - var addresses = Cache.GetOrCreate("fb14dfb9-9216-4b77-bfa9-7e85f8201ff4", entry => + var key = string.Concat("fb14dfb9-9216-4b77-bfa9-7e85f8201ff4", "\x1e", application.PostLogoutRedirectUris); + var addresses = Cache.GetOrCreate(key, entry => { entry.SetPriority(CacheItemPriority.High) .SetSlidingExpiration(TimeSpan.FromMinutes(1)); @@ -488,7 +490,8 @@ namespace OpenIddict.Stores // Note: parsing the stringified addresses is an expensive operation. // To mitigate that, the resulting array is stored in the memory cache. - var addresses = Cache.GetOrCreate("851d6f08-2ee0-4452-bbe5-ab864611ecaa", entry => + var key = string.Concat("851d6f08-2ee0-4452-bbe5-ab864611ecaa", "\x1e", application.RedirectUris); + var addresses = Cache.GetOrCreate(key, entry => { entry.SetPriority(CacheItemPriority.High) .SetSlidingExpiration(TimeSpan.FromMinutes(1)); diff --git a/src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs index c8a44ecb..cdfd2d27 100644 --- a/src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs @@ -162,6 +162,47 @@ namespace OpenIddict.Stores return ListAsync((scopes, values) => Query(scopes, values), names.ToArray(), cancellationToken); } + /// + /// Retrieves all the scopes that contain the specified resource. + /// + /// The resource associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified resource. + /// + public virtual async Task> FindByResourceAsync( + [NotNull] string resource, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); + } + + // To optimize the efficiency of the query a bit, only scopes whose stringified + // Resources column contains the specified resource are returned. Once the scopes + // are retrieved, a second pass is made to ensure only valid elements are returned. + // Implementers that use this method in a hot path may want to override this method + // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. + IQueryable Query(IQueryable scopes, string state) + => from scope in scopes + where scope.Resources.Contains(state) + select scope; + + var builder = ImmutableArray.CreateBuilder(); + + foreach (var application in await ListAsync((applications, state) => Query(applications, state), resource, cancellationToken)) + { + var resources = await GetResourcesAsync(application, cancellationToken); + if (resources.Contains(resource, StringComparer.OrdinalIgnoreCase)) + { + builder.Add(application); + } + } + + return builder.ToImmutable(); + } + /// /// Executes the specified query and returns the first element. /// @@ -301,7 +342,8 @@ namespace OpenIddict.Stores // Note: parsing the stringified resources is an expensive operation. // To mitigate that, the resulting array is stored in the memory cache. - var resources = Cache.GetOrCreate("b6148250-aede-4fb9-a621-07c9bcf238c3", entry => + var key = string.Concat("b6148250-aede-4fb9-a621-07c9bcf238c3", "\x1e", scope.Resources); + var resources = Cache.GetOrCreate(key, entry => { entry.SetPriority(CacheItemPriority.High) .SetSlidingExpiration(TimeSpan.FromMinutes(1));