/* * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * See https://github.com/openiddict/openiddict-core for more information concerning * the license and the contributors participating to this project. */ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using OpenIddict.Abstractions; namespace OpenIddict.Core { /// /// Provides methods allowing to cache authorizations after retrieving them from the store. /// /// The type of the Authorization entity. public class OpenIddictAuthorizationCache : IOpenIddictAuthorizationCache, IDisposable where TAuthorization : class { private readonly MemoryCache _cache; private readonly ConcurrentDictionary _signals; private readonly IOpenIddictAuthorizationStore _store; public OpenIddictAuthorizationCache( [NotNull] IOptionsMonitor options, [NotNull] IOpenIddictAuthorizationStoreResolver resolver) { _cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = options.CurrentValue.EntityCacheLimit }); _signals = new ConcurrentDictionary(StringComparer.Ordinal); _store = resolver.Get(); } /// /// Add the specified authorization to the cache. /// /// The authorization to add to the cache. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation. /// public async ValueTask AddAsync(TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } _cache.Remove(new { Method = nameof(FindAsync), Subject = await _store.GetSubjectAsync(authorization, cancellationToken), Client = await _store.GetApplicationIdAsync(authorization, cancellationToken) }); _cache.Remove(new { Method = nameof(FindAsync), Subject = await _store.GetSubjectAsync(authorization, cancellationToken), Client = await _store.GetApplicationIdAsync(authorization, cancellationToken), Status = await _store.GetStatusAsync(authorization, cancellationToken) }); _cache.Remove(new { Method = nameof(FindAsync), Subject = await _store.GetSubjectAsync(authorization, cancellationToken), Client = await _store.GetApplicationIdAsync(authorization, cancellationToken), Status = await _store.GetStatusAsync(authorization, cancellationToken), Type = await _store.GetTypeAsync(authorization, cancellationToken) }); _cache.Remove(new { Method = nameof(FindByApplicationIdAsync), Identifier = await _store.GetApplicationIdAsync(authorization, cancellationToken) }); _cache.Remove(new { Method = nameof(FindByIdAsync), Identifier = await _store.GetIdAsync(authorization, cancellationToken) }); _cache.Remove(new { Method = nameof(FindBySubjectAsync), Subject = await _store.GetSubjectAsync(authorization, cancellationToken) }); await CreateEntryAsync(new { Method = nameof(FindByIdAsync), Identifier = await _store.GetIdAsync(authorization, cancellationToken) }, authorization, cancellationToken); } /// /// Disposes the resources held by this instance. /// public void Dispose() { foreach (var signal in _signals) { signal.Value.Dispose(); } _cache.Dispose(); } /// /// Retrieves the authorizations corresponding to the specified /// subject and associated with the application identifier. /// /// The subject associated with the authorization. /// The client associated with the authorization. /// The that can be used to abort the operation. /// The authorizations corresponding to the subject/client. public IAsyncEnumerable FindAsync( [NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } if (string.IsNullOrEmpty(client)) { throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); } return ExecuteAsync(cancellationToken); async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) { var parameters = new { Method = nameof(FindAsync), Subject = subject, Client = client }; if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations)) { var builder = ImmutableArray.CreateBuilder(); await foreach (var authorization in _store.FindAsync(subject, client, cancellationToken)) { builder.Add(authorization); await AddAsync(authorization, cancellationToken); } authorizations = builder.ToImmutable(); await CreateEntryAsync(parameters, authorizations, cancellationToken); } foreach (var authorization in authorizations) { yield return authorization; } } } /// /// Retrieves the authorizations matching the specified parameters. /// /// The subject associated with the authorization. /// The client associated with the authorization. /// The authorization status. /// The that can be used to abort the operation. /// The authorizations corresponding to the criteria. public IAsyncEnumerable FindAsync( [NotNull] string subject, [NotNull] string client, [NotNull] string status, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } if (string.IsNullOrEmpty(client)) { throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); } if (string.IsNullOrEmpty(status)) { throw new ArgumentException("The status cannot be null or empty.", nameof(status)); } return ExecuteAsync(cancellationToken); async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) { var parameters = new { Method = nameof(FindAsync), Subject = subject, Client = client, Status = status }; if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations)) { var builder = ImmutableArray.CreateBuilder(); await foreach (var authorization in _store.FindAsync(subject, client, status, cancellationToken)) { builder.Add(authorization); await AddAsync(authorization, cancellationToken); } authorizations = builder.ToImmutable(); await CreateEntryAsync(parameters, authorizations, cancellationToken); } foreach (var authorization in authorizations) { yield return authorization; } } } /// /// Retrieves the authorizations matching the specified parameters. /// /// The subject associated with the authorization. /// The client associated with the authorization. /// The authorization status. /// The authorization type. /// The that can be used to abort the operation. /// The authorizations corresponding to the criteria. public IAsyncEnumerable FindAsync( [NotNull] string subject, [NotNull] string client, [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } if (string.IsNullOrEmpty(client)) { throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); } if (string.IsNullOrEmpty(status)) { throw new ArgumentException("The status cannot be null or empty.", nameof(status)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The type cannot be null or empty.", nameof(type)); } return ExecuteAsync(cancellationToken); async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) { var parameters = new { Method = nameof(FindAsync), Subject = subject, Client = client, Status = status, Type = type }; if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations)) { var builder = ImmutableArray.CreateBuilder(); await foreach (var authorization in _store.FindAsync(subject, client, status, type, cancellationToken)) { builder.Add(authorization); await AddAsync(authorization, cancellationToken); } authorizations = builder.ToImmutable(); await CreateEntryAsync(parameters, authorizations, cancellationToken); } foreach (var authorization in authorizations) { yield return authorization; } } } /// /// Retrieves the authorizations matching the specified parameters. /// /// The subject associated with the authorization. /// The client associated with the authorization. /// The authorization status. /// The authorization type. /// The minimal scopes associated with the authorization. /// The that can be used to abort the operation. /// The authorizations corresponding to the criteria. public IAsyncEnumerable FindAsync( [NotNull] string subject, [NotNull] string client, [NotNull] string status, [NotNull] string type, ImmutableArray scopes, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } if (string.IsNullOrEmpty(client)) { throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); } if (string.IsNullOrEmpty(status)) { throw new ArgumentException("The status cannot be null or empty.", nameof(status)); } if (string.IsNullOrEmpty(type)) { throw new ArgumentException("The type cannot be null or empty.", nameof(type)); } // Note: this method is only partially cached. return ExecuteAsync(cancellationToken); async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var authorization in _store.FindAsync(subject, client, status, type, scopes, cancellationToken)) { await AddAsync(authorization, cancellationToken); yield return authorization; } } } /// /// Retrieves the list of authorizations corresponding to the specified application identifier. /// /// The application identifier associated with the authorizations. /// The that can be used to abort the operation. /// The authorizations corresponding to the specified application. public IAsyncEnumerable FindByApplicationIdAsync( [NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } return ExecuteAsync(cancellationToken); async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) { var parameters = new { Method = nameof(FindByApplicationIdAsync), Identifier = identifier }; if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations)) { var builder = ImmutableArray.CreateBuilder(); await foreach (var authorization in _store.FindByApplicationIdAsync(identifier, cancellationToken)) { builder.Add(authorization); await AddAsync(authorization, cancellationToken); } authorizations = builder.ToImmutable(); await CreateEntryAsync(parameters, authorizations, cancellationToken); } foreach (var authorization in authorizations) { yield return authorization; } } } /// /// Retrieves an authorization using its unique identifier. /// /// The unique identifier associated with the authorization. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorization corresponding to the identifier. /// public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } var parameters = new { Method = nameof(FindByIdAsync), Identifier = identifier }; if (_cache.TryGetValue(parameters, out TAuthorization authorization)) { return new ValueTask(authorization); } return new ValueTask(ExecuteAsync()); async Task ExecuteAsync() { if ((authorization = await _store.FindByIdAsync(identifier, cancellationToken)) != null) { await AddAsync(authorization, cancellationToken); } await CreateEntryAsync(parameters, authorization, cancellationToken); return authorization; } } /// /// Retrieves all the authorizations corresponding to the specified subject. /// /// The subject associated with the authorization. /// The that can be used to abort the operation. /// The authorizations corresponding to the specified subject. public IAsyncEnumerable FindBySubjectAsync( [NotNull] string subject, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } return ExecuteAsync(cancellationToken); async IAsyncEnumerable ExecuteAsync([EnumeratorCancellation] CancellationToken cancellationToken) { var parameters = new { Method = nameof(FindBySubjectAsync), Subject = subject }; if (!_cache.TryGetValue(parameters, out ImmutableArray authorizations)) { var builder = ImmutableArray.CreateBuilder(); await foreach (var authorization in _store.FindBySubjectAsync(subject, cancellationToken)) { builder.Add(authorization); await AddAsync(authorization, cancellationToken); } authorizations = builder.ToImmutable(); await CreateEntryAsync(parameters, authorizations, cancellationToken); } foreach (var authorization in authorizations) { yield return authorization; } } } /// /// Removes the specified authorization from the cache. /// /// The authorization to remove from the cache. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. public async ValueTask RemoveAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } var identifier = await _store.GetIdAsync(authorization, cancellationToken); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The application identifier cannot be extracted."); } if (_signals.TryRemove(identifier, out CancellationTokenSource signal)) { signal.Cancel(); signal.Dispose(); } } /// /// Creates a cache entry for the specified key. /// /// The cache key. /// The authorization to store in the cache entry, if applicable. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. protected virtual async ValueTask CreateEntryAsync( [NotNull] object key, [CanBeNull] TAuthorization authorization, CancellationToken cancellationToken) { if (key == null) { throw new ArgumentNullException(nameof(key)); } using var entry = _cache.CreateEntry(key); if (authorization != null) { var signal = await CreateExpirationSignalAsync(authorization, cancellationToken); if (signal == null) { throw new InvalidOperationException("An error occurred while creating an expiration signal."); } entry.AddExpirationToken(signal); } entry.SetSize(1L); entry.SetValue(authorization); } /// /// Creates a cache entry for the specified key. /// /// The cache key. /// The authorizations to store in the cache entry. /// The that can be used to abort the operation. /// A that can be used to monitor the asynchronous operation. protected virtual async ValueTask CreateEntryAsync( [NotNull] object key, [CanBeNull] ImmutableArray authorizations, CancellationToken cancellationToken) { if (key == null) { throw new ArgumentNullException(nameof(key)); } using var entry = _cache.CreateEntry(key); foreach (var authorization in authorizations) { var signal = await CreateExpirationSignalAsync(authorization, cancellationToken); if (signal == null) { throw new InvalidOperationException("An error occurred while creating an expiration signal."); } entry.AddExpirationToken(signal); } entry.SetSize(authorizations.Length); entry.SetValue(authorizations); } /// /// Creates an expiration signal allowing to invalidate all the /// cache entries associated with the specified authorization. /// /// The authorization associated with the expiration signal. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns an expiration signal for the specified authorization. /// protected virtual async ValueTask CreateExpirationSignalAsync( [NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } var identifier = await _store.GetIdAsync(authorization, cancellationToken); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The authorization identifier cannot be extracted."); } var signal = _signals.GetOrAdd(identifier, _ => new CancellationTokenSource()); return new CancellationChangeToken(signal.Token); } } }