Skip to content

Commit

Permalink
Refactor authorization client cache implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ciprianjichici committed Dec 12, 2024
1 parent 647746b commit 32640a6
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 94 deletions.
48 changes: 17 additions & 31 deletions src/dotnet/Common/Clients/AuthorizationServiceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using FoundationaLLM.Common.Models.Authorization;
using FoundationaLLM.Common.Models.Configuration.Authorization;
using FoundationaLLM.Common.Models.ResourceProviders.Authorization;
using FoundationaLLM.Common.Services.Cache;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
Expand All @@ -19,26 +20,29 @@ public class AuthorizationServiceClient : IAuthorizationServiceClient
{
private readonly AuthorizationServiceClientSettings _settings;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IAuthorizationServiceClientCacheService _cacheService;
private readonly ILogger<AuthorizationServiceClient> _logger;
private readonly ILogger<AuthorizationServiceClient> _logger;

private readonly IAuthorizationServiceClientCacheService? _cacheService;

/// <summary>
/// Initializes a new instance of the <see cref="AuthorizationServiceClient"/> class.
/// </summary>
/// <param name="httpClientFactory">The centralized factory from which to create HTTP clients.</param>
/// <param name="options"><see cref="AuthorizationServiceClientSettings"/> options.</param>
/// <param name="cacheService">The cache service used to store authorization results for quick retrieval.</param>
/// <param name="logger">The logger used for logging.</param>
public AuthorizationServiceClient(
IHttpClientFactory httpClientFactory,
IOptions<AuthorizationServiceClientSettings> options,
IAuthorizationServiceClientCacheService cacheService,
ILogger<AuthorizationServiceClient> logger)
{
_settings = options.Value;
_httpClientFactory = httpClientFactory;
_cacheService = cacheService;
_logger = logger;

if (_settings.EnableCache)
{
_cacheService = new AuthorizationServiceClientCacheService(logger);
}
}

/// <inheritdoc/>
Expand All @@ -62,27 +66,6 @@ public async Task<ActionAuthorizationResult> ProcessAuthorizationRequest(

try
{
string cacheKey = string.Empty;

if (_settings.EnableCache)
{
cacheKey = _cacheService.GenerateCacheKey(
instanceId,
action,
resourcePaths,
expandResourceTypePaths,
includeRoleAssignments,
includeActions,
userIdentity);

_cacheService.TryGetValue(cacheKey, out ActionAuthorizationResult? cachedResult);

if (cachedResult != null)
{
return cachedResult;
}
}

var authorizationRequest = new ActionAuthorizationRequest
{
Action = action,
Expand All @@ -98,6 +81,11 @@ public async Task<ActionAuthorizationResult> ProcessAuthorizationRequest(
}
};

if (_cacheService != null
&& _cacheService.TryGetValue(authorizationRequest, out var cachedAuthorizationResponse)
&& cachedAuthorizationResponse != null)
return cachedAuthorizationResponse;

var httpClient = await CreateHttpClient();
var response = await httpClient.PostAsync(
$"/instances/{instanceId}/authorize",
Expand All @@ -109,11 +97,9 @@ public async Task<ActionAuthorizationResult> ProcessAuthorizationRequest(
var result = JsonSerializer.Deserialize<ActionAuthorizationResult>(responseContent);
if (result != null)
{
if (_settings.EnableCache)
{
_cacheService.SetValue(cacheKey, result);
}

// Try to cache the result if we have a cache service active.
_cacheService?.SetValue(authorizationRequest, result);

return result;
}
_logger.LogError("The response from the Authorization API was invalid and could not be parsed.");
Expand Down
4 changes: 2 additions & 2 deletions src/dotnet/Common/Clients/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static void AddAuthorizationServiceClient(this IHostApplicationBuilder bu
{
builder.Services.AddOptions<AuthorizationServiceClientSettings>()
.Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_APIEndpoints_AuthorizationAPI_Essentials));
builder.Services.AddSingleton<IAuthorizationServiceClientCacheService, AuthorizationServiceClientMemoryCacheService>();
builder.Services.AddSingleton<IAuthorizationServiceClientCacheService, AuthorizationServiceClientCacheService>();
builder.Services.AddSingleton<IAuthorizationServiceClient, AuthorizationServiceClient>();
}

Expand All @@ -37,7 +37,7 @@ public static void AddAuthorizationServiceClient(this IServiceCollection service
{
services.AddOptions<AuthorizationServiceClientSettings>()
.Bind(configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_APIEndpoints_AuthorizationAPI_Essentials));
services.AddSingleton<IAuthorizationServiceClientCacheService, AuthorizationServiceClientMemoryCacheService>();
services.AddSingleton<IAuthorizationServiceClientCacheService, AuthorizationServiceClientCacheService>();
services.AddSingleton<IAuthorizationServiceClient, AuthorizationServiceClient>();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using FoundationaLLM.Common.Models.Authentication;
using FoundationaLLM.Common.Models.Authorization;
using FoundationaLLM.Common.Models.Authorization;

namespace FoundationaLLM.Common.Interfaces
{
Expand All @@ -11,36 +10,16 @@ public interface IAuthorizationServiceClientCacheService
/// <summary>
/// Attempts to retrieve an <see cref="ActionAuthorizationResult"/> from the cache.
/// </summary>
/// <param name="cacheKey">The key used to identify the resource value in the cache.</param>
/// <param name="result">The <see cref="ActionAuthorizationResult"/> to be retrieved.</param>
/// <param name="authorizationRequest">The <see cref="ActionAuthorizationRequest"/> key used to identify the authorization result in the cache.</param>
/// <param name="authorizationResult">The <see cref="ActionAuthorizationResult"/> to be retrieved.</param>
/// <returns><see langword="true"/> if the value was found in the cache, <see langword="false"/> otherwise.</returns>
bool TryGetValue(string cacheKey, out ActionAuthorizationResult? result);
bool TryGetValue(ActionAuthorizationRequest authorizationRequest, out ActionAuthorizationResult? authorizationResult);

/// <summary>
/// Sets an <see cref="ActionAuthorizationResult"/> in the cache.
/// </summary>
/// <param name="cacheKey">The key used to identify the resource value in the cache.</param>
/// <param name="result">The <see cref="ActionAuthorizationResult"/> value to be set in the cache.</param>
void SetValue(string cacheKey, ActionAuthorizationResult result);

/// <summary>
/// Generates a cache key based on the provided parameters representing an authorization request.
/// </summary>
/// <param name="instanceId">The FoundationaLLM instance idenitifier.</param>
/// <param name="action">The action that was authorized.</param>
/// <param name="resourcePaths">The resource path(s) being authorized.</param>
/// <param name="expandResourceTypePaths">Setting to deterime if resource type paths should be expanded.</param>
/// <param name="includeRoleAssignments">Setting to determine if role assignments are included as part of the authorization.</param>
/// <param name="includeActions">Setting to determine if actions are included as part of the authorization.</param>
/// <param name="userIdentity">The identity of the process initiating the authorization process.</param>
/// <returns>The generated cache key for the authorization built from the parameters passed in.</returns>
string GenerateCacheKey(
string instanceId,
string action,
List<string> resourcePaths,
bool expandResourceTypePaths,
bool includeRoleAssignments,
bool includeActions,
UnifiedUserIdentity userIdentity);
/// <param name="authorizationRequest">The <see cref="ActionAuthorizationRequest"/> key used to set the authorization result in the cache.</param>
/// <param name="authorizationResult">The <see cref="ActionAuthorizationResult"/> to be set.</param>
void SetValue(ActionAuthorizationRequest authorizationRequest, ActionAuthorizationResult authorizationResult);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using FoundationaLLM.Common.Interfaces;
using FoundationaLLM.Common.Models.Authentication;
using FoundationaLLM.Common.Models.Authorization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
Expand All @@ -12,11 +11,10 @@ namespace FoundationaLLM.Common.Services.Cache
/// Provides the caching services used by the FoundationaLLM authorization service client.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> used to log information.</param>
public class AuthorizationServiceClientMemoryCacheService(
ILogger<AuthorizationServiceClientMemoryCacheService> logger) : IAuthorizationServiceClientCacheService
public class AuthorizationServiceClientCacheService(
ILogger logger) : IAuthorizationServiceClientCacheService
{
private readonly ILogger _logger = logger;

private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 10000, // Limit cache size to 10000 resources.
Expand All @@ -29,15 +27,18 @@ public class AuthorizationServiceClientMemoryCacheService(
.SetSize(1); // Each cache entry is a single authorization result.

private readonly SemaphoreSlim _cacheLock = new(1, 1);
private readonly MD5 _md5 = MD5.Create();

/// <inheritdoc/>
public async void SetValue(string cacheKey, ActionAuthorizationResult result)
public async void SetValue(ActionAuthorizationRequest authorizationRequest, ActionAuthorizationResult result)
{
await _cacheLock.WaitAsync();
try
{
_cache.Set(cacheKey, result, _cacheEntryOptions);
_logger.LogInformation("The authorization result has been set in the cache.");
_cache.Set(GetCacheKey(authorizationRequest), result, _cacheEntryOptions);
_logger.LogInformation(
"An action authorization result for the authorizable action {AuthorizableAction} has been set in the cache.",
authorizationRequest.Action);
}
catch (Exception ex)
{
Expand All @@ -50,15 +51,15 @@ public async void SetValue(string cacheKey, ActionAuthorizationResult result)
}

/// <inheritdoc/>
public bool TryGetValue(string cacheKey, out ActionAuthorizationResult? result)
public bool TryGetValue(ActionAuthorizationRequest authorizationRequest, out ActionAuthorizationResult? authorizationResult)
{
result = default;
authorizationResult = default;
try
{
if (_cache.TryGetValue(cacheKey, out ActionAuthorizationResult? cachedValue)
if (_cache.TryGetValue(GetCacheKey(authorizationRequest), out ActionAuthorizationResult? cachedValue)
&& cachedValue != null)
{
result = cachedValue;
authorizationResult = cachedValue;
_logger.LogInformation("Cache hit for the authorization request");
return true;
}
Expand All @@ -67,26 +68,21 @@ public bool TryGetValue(string cacheKey, out ActionAuthorizationResult? result)
{
_logger.LogError(ex, "There was an error getting the ActionAuthorizationResult from the cache.");
}

return false;
}

/// <inheritdoc/>
public string GenerateCacheKey(
string instanceId,
string action,
List<string> resourcePaths,
bool expandResourceTypePaths,
bool includeRoleAssignments,
bool includeActions,
UnifiedUserIdentity userIdentity)
private string GetCacheKey(
ActionAuthorizationRequest authorizationRequest)
{
var sortedResourcePaths = string.Join(",", resourcePaths.Distinct().OrderBy(rp => rp));
var resourcePaths = string.Join(",", authorizationRequest.ResourcePaths);
var groupIds = string.Join(",", authorizationRequest.UserContext.SecurityGroupIds);
var userIdentity = $"{authorizationRequest.UserContext.SecurityPrincipalId}:{authorizationRequest.UserContext.UserPrincipalName}:{groupIds}";

var keyString = $"{instanceId}:{action}:{sortedResourcePaths}:{expandResourceTypePaths}:{includeRoleAssignments}:{includeActions}:{userIdentity.UserId}:{userIdentity.UPN}:{string.Join(",", userIdentity.GroupIds)}";
var keyString = $"{authorizationRequest.Action}:{resourcePaths}:{authorizationRequest.ExpandResourceTypePaths}:{authorizationRequest.IncludeRoles}:{authorizationRequest.IncludeActions}:{userIdentity}";

using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyString));
var hashBytes = _md5.ComputeHash(Encoding.UTF8.GetBytes(keyString));
return Convert.ToBase64String(hashBytes);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;

namespace FoundationaLLM.Common.Services.ResourceProviders
namespace FoundationaLLM.Common.Services.Cache
{
/// <summary>
/// Provides the resource caching services used by FoundationaLLM resource providers.
Expand All @@ -15,10 +15,10 @@ public class ResourceProviderResourceCacheService(
private readonly ILogger _logger = logger;

private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 10000, // Limit cache size to 5000 resources.
ExpirationScanFrequency = TimeSpan.FromMinutes(5) // Scan for expired items every five minutes.
});
{
SizeLimit = 10000, // Limit cache size to 5000 resources.
ExpirationScanFrequency = TimeSpan.FromMinutes(5) // Scan for expired items every five minutes.
});
private readonly MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(60)) // Cache entries are valid for 60 minutes.
.SetSlidingExpiration(TimeSpan.FromMinutes(30)) // Reset expiration time if accessed within 5 minutes.
Expand All @@ -29,7 +29,7 @@ public void SetValue<T>(ResourceReference resourceReference, T resourceValue) wh
{
try
{
_cache.Set<T>(GetCacheKey(resourceReference), resourceValue, _cacheEntryOptions);
_cache.Set(GetCacheKey(resourceReference), resourceValue, _cacheEntryOptions);
_logger.LogInformation("The resource {ResourceName} of type {ResourceType} has been set in the cache.",
resourceReference.Name,
resourceReference.Type);
Expand All @@ -49,7 +49,7 @@ public bool TryGetValue<T>(ResourceReference resourceReference, out T? resourceV

try
{
if (_cache.TryGetValue<T>(GetCacheKey(resourceReference), out T? cachedValue)
if (_cache.TryGetValue(GetCacheKey(resourceReference), out T? cachedValue)
&& cachedValue != null)
{
resourceValue = cachedValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
using FoundationaLLM.Common.Models.Configuration.Instance;
using FoundationaLLM.Common.Models.Events;
using FoundationaLLM.Common.Models.ResourceProviders;
using FoundationaLLM.Common.Services.Cache;
using FoundationaLLM.Common.Services.Events;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Immutable;
Expand Down

0 comments on commit 32640a6

Please sign in to comment.