mirror of
https://github.com/bitwarden/server.git
synced 2026-05-04 06:11:47 -05:00
[PM-32069] Add ExtendedProviderAbilityCacheService
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace Bit.Core.AdminConsole.AbilitiesCache;
|
||||
|
||||
public class ExtendedProviderAbilityCacheService(
|
||||
[FromKeyedServices(ExtendedProviderAbilityCacheService.CacheName)] IFusionCache cache,
|
||||
IProviderRepository providerRepository)
|
||||
: IProviderAbilityCacheService
|
||||
{
|
||||
public const string CacheName = "ProviderAbilities";
|
||||
|
||||
public async Task<ProviderAbility?> GetProviderAbilityAsync(Guid providerId)
|
||||
{
|
||||
return await cache.GetOrSetAsync<ProviderAbility?>(
|
||||
$"{providerId}",
|
||||
async _ => await providerRepository.GetAbilityAsync(providerId)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task UpsertProviderAbilityAsync(Provider provider)
|
||||
{
|
||||
await cache.SetAsync($"{provider.Id}", new ProviderAbility(provider));
|
||||
}
|
||||
|
||||
public async Task DeleteProviderAbilityAsync(Guid providerId)
|
||||
{
|
||||
await cache.RemoveAsync($"{providerId}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
|
||||
namespace Bit.Core.AdminConsole.AbilitiesCache;
|
||||
|
||||
public interface IProviderAbilityCacheService
|
||||
{
|
||||
Task<ProviderAbility?> GetProviderAbilityAsync(Guid providerId);
|
||||
Task UpsertProviderAbilityAsync(Provider provider);
|
||||
Task DeleteProviderAbilityAsync(Guid providerId);
|
||||
}
|
||||
@@ -165,6 +165,7 @@ public static class FeatureFlagKeys
|
||||
public const string PublicMembersInviteRefactor = "pm-33398-refactor-members-invite-org-users-command";
|
||||
public const string GenerateInviteLink = "pm-32497-generate-invite-link";
|
||||
public const string OrgAbilityExtendedCache = "pm-32104-org-ability-extended-cache";
|
||||
public const string ProviderAbilityExtendedCache = "pm-32111-provider-ability-extended-cache";
|
||||
|
||||
/* Architecture */
|
||||
public const string DesktopMigrationMilestone1 = "desktop-ui-migration-milestone-1";
|
||||
|
||||
@@ -7,7 +7,9 @@ using Bit.Core.Models.Data.Organizations;
|
||||
namespace Bit.Core.Services.Implementations;
|
||||
|
||||
public class FeatureRoutedCacheService(
|
||||
IVCurrentInMemoryApplicationCacheService inMemoryApplicationCacheService)
|
||||
IVCurrentInMemoryApplicationCacheService inMemoryApplicationCacheService,
|
||||
IProviderAbilityCacheService providerAbilityCacheService,
|
||||
IFeatureService featureService)
|
||||
: IApplicationCacheService
|
||||
{
|
||||
public Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync() =>
|
||||
@@ -21,6 +23,11 @@ public class FeatureRoutedCacheService(
|
||||
|
||||
public async Task<ProviderAbility?> GetProviderAbilityAsync(Guid providerId)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache))
|
||||
{
|
||||
return await providerAbilityCacheService.GetProviderAbilityAsync(providerId);
|
||||
}
|
||||
|
||||
(await GetProviderAbilitiesAsync([providerId])).TryGetValue(providerId, out var providerAbility);
|
||||
return providerAbility;
|
||||
}
|
||||
@@ -46,14 +53,28 @@ public class FeatureRoutedCacheService(
|
||||
public Task UpsertOrganizationAbilityAsync(Organization organization) =>
|
||||
inMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
public Task UpsertProviderAbilityAsync(Provider provider) =>
|
||||
inMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
public Task UpsertProviderAbilityAsync(Provider provider)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache))
|
||||
{
|
||||
return providerAbilityCacheService.UpsertProviderAbilityAsync(provider);
|
||||
}
|
||||
|
||||
return inMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider);
|
||||
}
|
||||
|
||||
public Task DeleteOrganizationAbilityAsync(Guid organizationId) =>
|
||||
inMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId);
|
||||
|
||||
public Task DeleteProviderAbilityAsync(Guid providerId) =>
|
||||
inMemoryApplicationCacheService.DeleteProviderAbilityAsync(providerId);
|
||||
public Task DeleteProviderAbilityAsync(Guid providerId)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache))
|
||||
{
|
||||
return providerAbilityCacheService.DeleteProviderAbilityAsync(providerId);
|
||||
}
|
||||
|
||||
return inMemoryApplicationCacheService.DeleteProviderAbilityAsync(providerId);
|
||||
}
|
||||
|
||||
public async Task BaseUpsertOrganizationAbilityAsync(Organization organization)
|
||||
{
|
||||
|
||||
@@ -294,6 +294,10 @@ public static class ServiceCollectionExtensions
|
||||
services.AddOptionality();
|
||||
services.AddTokenizers();
|
||||
|
||||
services.AddDistributedCache(globalSettings);
|
||||
services.AddExtendedCache(ExtendedProviderAbilityCacheService.CacheName, globalSettings);
|
||||
services.AddSingleton<IProviderAbilityCacheService, ExtendedProviderAbilityCacheService>();
|
||||
|
||||
services.AddScoped<IApplicationCacheService, FeatureRoutedCacheService>();
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
using Bit.Core.AdminConsole.AbilitiesCache;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
using ProviderEntity = Bit.Core.AdminConsole.Entities.Provider.Provider;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.AbilitiesCache;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class ExtendedProviderAbilityCacheServiceTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilityAsync_OnCacheHit_ReturnsWithoutCallingRepository(
|
||||
SutProvider<ExtendedProviderAbilityCacheService> sutProvider,
|
||||
ProviderAbility cachedAbility)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFusionCache>()
|
||||
.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<ProviderAbility?>, CancellationToken, Task<ProviderAbility?>>>(),
|
||||
options: Arg.Any<FusionCacheEntryOptions?>(),
|
||||
tags: Arg.Any<IEnumerable<string>?>())
|
||||
.Returns(new ValueTask<ProviderAbility?>(cachedAbility));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilityAsync(cachedAbility.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(cachedAbility, result);
|
||||
await sutProvider.GetDependency<IProviderRepository>()
|
||||
.DidNotReceive()
|
||||
.GetAbilityAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IFusionCache>()
|
||||
.Received(1)
|
||||
.GetOrSetAsync<ProviderAbility?>(
|
||||
Arg.Is<string>(k => k == cachedAbility.Id.ToString()),
|
||||
Arg.Any<Func<FusionCacheFactoryExecutionContext<ProviderAbility?>, CancellationToken, Task<ProviderAbility?>>>(),
|
||||
Arg.Any<FusionCacheEntryOptions?>(),
|
||||
Arg.Any<IEnumerable<string>?>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilityAsync_OnCacheMiss_QueriesRepositoryAndCaches(
|
||||
SutProvider<ExtendedProviderAbilityCacheService> sutProvider,
|
||||
Guid providerId,
|
||||
ProviderAbility repositoryAbility)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderRepository>()
|
||||
.GetAbilityAsync(providerId)
|
||||
.Returns(repositoryAbility);
|
||||
|
||||
sutProvider.GetDependency<IFusionCache>()
|
||||
.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<ProviderAbility?>, CancellationToken, Task<ProviderAbility?>>>(),
|
||||
options: Arg.Any<FusionCacheEntryOptions?>(),
|
||||
tags: Arg.Any<IEnumerable<string>?>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var factory = callInfo.ArgAt<Func<FusionCacheFactoryExecutionContext<ProviderAbility?>, CancellationToken, Task<ProviderAbility?>>>(1);
|
||||
return new ValueTask<ProviderAbility?>(factory.Invoke(null!, CancellationToken.None));
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(repositoryAbility, result);
|
||||
await sutProvider.GetDependency<IProviderRepository>()
|
||||
.Received(1)
|
||||
.GetAbilityAsync(providerId);
|
||||
await sutProvider.GetDependency<IFusionCache>()
|
||||
.Received(1)
|
||||
.GetOrSetAsync<ProviderAbility?>(
|
||||
Arg.Is<string>(k => k == providerId.ToString()),
|
||||
Arg.Any<Func<FusionCacheFactoryExecutionContext<ProviderAbility?>, CancellationToken, Task<ProviderAbility?>>>(),
|
||||
Arg.Any<FusionCacheEntryOptions?>(),
|
||||
Arg.Any<IEnumerable<string>?>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilityAsync_WhenProviderDoesNotExist_ReturnsNullAndCachesIt(
|
||||
SutProvider<ExtendedProviderAbilityCacheService> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderRepository>()
|
||||
.GetAbilityAsync(providerId)
|
||||
.Returns((ProviderAbility?)null);
|
||||
|
||||
sutProvider.GetDependency<IFusionCache>()
|
||||
.GetOrSetAsync(
|
||||
key: Arg.Any<string>(),
|
||||
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<ProviderAbility?>, CancellationToken, Task<ProviderAbility?>>>(),
|
||||
options: Arg.Any<FusionCacheEntryOptions?>(),
|
||||
tags: Arg.Any<IEnumerable<string>?>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var factory = callInfo.ArgAt<Func<FusionCacheFactoryExecutionContext<ProviderAbility?>, CancellationToken, Task<ProviderAbility?>>>(1);
|
||||
return new ValueTask<ProviderAbility?>(factory.Invoke(null!, CancellationToken.None));
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert - null is cached to prevent database thrashing for non-existent providers
|
||||
Assert.Null(result);
|
||||
await sutProvider.GetDependency<IProviderRepository>()
|
||||
.Received(1)
|
||||
.GetAbilityAsync(providerId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertProviderAbilityAsync_SetsUpdatedAbilityInCache(
|
||||
SutProvider<ExtendedProviderAbilityCacheService> sutProvider,
|
||||
ProviderEntity provider)
|
||||
{
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Assert - SetAsync updates the cache entry directly with the new data
|
||||
await sutProvider.GetDependency<IFusionCache>()
|
||||
.Received(1)
|
||||
.SetAsync(
|
||||
Arg.Is<string>(k => k == provider.Id.ToString()),
|
||||
Arg.Is<ProviderAbility>(a => a.Id == provider.Id),
|
||||
Arg.Any<FusionCacheEntryOptions?>(),
|
||||
Arg.Any<IEnumerable<string>?>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteProviderAbilityAsync_RemovesCacheEntry(
|
||||
SutProvider<ExtendedProviderAbilityCacheService> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IFusionCache>()
|
||||
.Received(1)
|
||||
.RemoveAsync(
|
||||
Arg.Is<string>(k => k == providerId.ToString()),
|
||||
Arg.Any<FusionCacheEntryOptions?>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -297,7 +297,10 @@ public class FeatureRoutedCacheServiceTests
|
||||
{
|
||||
// Arrange
|
||||
var currentCacheService = CreateCurrentCacheMockService();
|
||||
var sut = new FeatureRoutedCacheService(currentCacheService);
|
||||
var sut = new FeatureRoutedCacheService(
|
||||
currentCacheService,
|
||||
Substitute.For<IProviderAbilityCacheService>(),
|
||||
Substitute.For<IFeatureService>());
|
||||
|
||||
// Act
|
||||
await sut.BaseUpsertOrganizationAbilityAsync(organization);
|
||||
@@ -327,7 +330,10 @@ public class FeatureRoutedCacheServiceTests
|
||||
{
|
||||
// Arrange
|
||||
var currentCacheService = CreateCurrentCacheMockService();
|
||||
var sut = new FeatureRoutedCacheService(currentCacheService);
|
||||
var sut = new FeatureRoutedCacheService(
|
||||
currentCacheService,
|
||||
Substitute.For<IProviderAbilityCacheService>(),
|
||||
Substitute.For<IFeatureService>());
|
||||
|
||||
// Act
|
||||
await sut.BaseDeleteOrganizationAbilityAsync(organizationId);
|
||||
@@ -351,6 +357,148 @@ public class FeatureRoutedCacheServiceTests
|
||||
Assert.Equal(ExpectedErrorMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilityAsync_WhenFeatureFlagEnabled_UsesExtendedCacheService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid providerId,
|
||||
ProviderAbility expectedAbility)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IProviderAbilityCacheService>()
|
||||
.GetProviderAbilityAsync(providerId)
|
||||
.Returns(expectedAbility);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedAbility, result);
|
||||
await sutProvider.GetDependency<IProviderAbilityCacheService>()
|
||||
.Received(1)
|
||||
.GetProviderAbilityAsync(providerId);
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetProviderAbilityAsync_WhenFeatureFlagDisabled_UsesInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
ProviderAbility providerAbility)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache)
|
||||
.Returns(false);
|
||||
var allAbilities = new Dictionary<Guid, ProviderAbility> { [providerAbility.Id] = providerAbility };
|
||||
sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.GetProviderAbilitiesAsync()
|
||||
.Returns(allAbilities);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetProviderAbilityAsync(providerAbility.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(providerAbility, result);
|
||||
await sutProvider.GetDependency<IProviderAbilityCacheService>()
|
||||
.DidNotReceive()
|
||||
.GetProviderAbilityAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.GetProviderAbilitiesAsync();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertProviderAbilityAsync_WhenFeatureFlagEnabled_UsesExtendedCacheService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Provider provider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IProviderAbilityCacheService>()
|
||||
.Received(1)
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.UpsertProviderAbilityAsync(Arg.Any<Provider>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpsertProviderAbilityAsync_WhenFeatureFlagDisabled_UsesInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Provider provider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpsertProviderAbilityAsync(provider);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.UpsertProviderAbilityAsync(provider);
|
||||
await sutProvider.GetDependency<IProviderAbilityCacheService>()
|
||||
.DidNotReceive()
|
||||
.UpsertProviderAbilityAsync(Arg.Any<Provider>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteProviderAbilityAsync_WhenFeatureFlagEnabled_UsesExtendedCacheService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IProviderAbilityCacheService>()
|
||||
.Received(1)
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteProviderAbilityAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteProviderAbilityAsync_WhenFeatureFlagDisabled_UsesInMemoryService(
|
||||
SutProvider<FeatureRoutedCacheService> sutProvider,
|
||||
Guid providerId)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.ProviderAbilityExtendedCache)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()
|
||||
.Received(1)
|
||||
.DeleteProviderAbilityAsync(providerId);
|
||||
await sutProvider.GetDependency<IProviderAbilityCacheService>()
|
||||
.DidNotReceive()
|
||||
.DeleteProviderAbilityAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Our SUT uses a method that is not part of IVCurrentInMemoryApplicationCacheService,
|
||||
/// so AutoFixture's auto-created mock won't work.
|
||||
|
||||
Reference in New Issue
Block a user