1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-22 02:51:33 +01:00

remove the redis grant store (#3757)

This commit is contained in:
Kyle Spearrin 2024-02-07 14:50:23 -05:00 committed by GitHub
parent a019355ab4
commit f0a8fd63ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 13 additions and 253 deletions

View File

@ -3,19 +3,15 @@ using Bit.Identity.IdentityServer;
using Bit.Infrastructure.Dapper.Auth.Repositories;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Stores;
using Microsoft.Extensions.Logging.Abstractions;
using StackExchange.Redis;
namespace Bit.MicroBenchmarks.Identity.IdentityServer;
[MemoryDiagnoser]
public class RedisPersistedGrantStoreTests
public class PersistedGrantStoreTests
{
const string SQL = nameof(SQL);
const string Redis = nameof(Redis);
const string Cosmos = nameof(Cosmos);
private readonly IPersistedGrantStore _redisGrantStore;
private readonly IPersistedGrantStore _sqlGrantStore;
private readonly IPersistedGrantStore _cosmosGrantStore;
private readonly PersistedGrant _updateGrant;
@ -39,14 +35,8 @@ public class RedisPersistedGrantStoreTests
// 15) "ClientId"
// 16) "web"
public RedisPersistedGrantStoreTests()
public PersistedGrantStoreTests()
{
_redisGrantStore = new RedisPersistedGrantStore(
ConnectionMultiplexer.Connect("localhost"),
NullLogger<RedisPersistedGrantStore>.Instance,
new InMemoryPersistedGrantStore()
);
var sqlConnectionString = "YOUR CONNECTION STRING HERE";
_sqlGrantStore = new PersistedGrantStore(
new GrantRepository(
@ -78,17 +68,13 @@ public class RedisPersistedGrantStoreTests
};
}
[Params(Redis, SQL, Cosmos)]
[Params(SQL, Cosmos)]
public string StoreType { get; set; } = null!;
[GlobalSetup]
public void Setup()
{
if (StoreType == Redis)
{
_grantStore = _redisGrantStore;
}
else if (StoreType == SQL)
if (StoreType == SQL)
{
_grantStore = _sqlGrantStore;
}

View File

@ -9,16 +9,13 @@ public class PersistedGrantStore : IPersistedGrantStore
{
private readonly IGrantRepository _grantRepository;
private readonly Func<PersistedGrant, IGrant> _toGrant;
private readonly IPersistedGrantStore _fallbackGrantStore;
public PersistedGrantStore(
IGrantRepository grantRepository,
Func<PersistedGrant, IGrant> toGrant,
IPersistedGrantStore fallbackGrantStore = null)
Func<PersistedGrant, IGrant> toGrant)
{
_grantRepository = grantRepository;
_toGrant = toGrant;
_fallbackGrantStore = fallbackGrantStore;
}
public async Task<PersistedGrant> GetAsync(string key)
@ -26,11 +23,6 @@ public class PersistedGrantStore : IPersistedGrantStore
var grant = await _grantRepository.GetByKeyAsync(key);
if (grant == null)
{
if (_fallbackGrantStore != null)
{
// It wasn't found, there is a chance is was instead stored in the fallback store
return await _fallbackGrantStore.GetAsync(key);
}
return null;
}

View File

@ -1,181 +0,0 @@
using System.Diagnostics;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Stores;
using MessagePack;
using StackExchange.Redis;
namespace Bit.Identity.IdentityServer;
/// <summary>
/// A <see cref="IPersistedGrantStore"/> that persists its grants on a Redis DB
/// </summary>
/// <remarks>
/// This store also allows a fallback to another store in the case that a key was not found
/// in the Redis DB or the Redis DB happens to be down.
/// </remarks>
public class RedisPersistedGrantStore : IPersistedGrantStore
{
private static readonly MessagePackSerializerOptions _options = MessagePackSerializerOptions.Standard;
private readonly IConnectionMultiplexer _connectionMultiplexer;
private readonly ILogger<RedisPersistedGrantStore> _logger;
private readonly IPersistedGrantStore _fallbackGrantStore;
public RedisPersistedGrantStore(
IConnectionMultiplexer connectionMultiplexer,
ILogger<RedisPersistedGrantStore> logger,
IPersistedGrantStore fallbackGrantStore)
{
_connectionMultiplexer = connectionMultiplexer;
_logger = logger;
_fallbackGrantStore = fallbackGrantStore;
}
public Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)
{
_logger.LogWarning("Redis does not implement 'GetAllAsync', Skipping.");
return Task.FromResult(Enumerable.Empty<PersistedGrant>());
}
public async Task<PersistedGrant> GetAsync(string key)
{
try
{
if (!_connectionMultiplexer.IsConnected)
{
// Redis is down, fallback to using SQL table
_logger.LogWarning("This is not connected, using fallback store to execute 'GetAsync' with {Key}.", key);
return await _fallbackGrantStore.GetAsync(key);
}
var redisKey = CreateRedisKey(key);
var redisDb = _connectionMultiplexer.GetDatabase();
var redisValueAndExpiry = await redisDb.StringGetWithExpiryAsync(redisKey);
if (!redisValueAndExpiry.Value.HasValue)
{
// It wasn't found, there is a chance is was instead stored in the fallback store
_logger.LogWarning("Could not find grant in primary store, using fallback one.");
return await _fallbackGrantStore.GetAsync(key);
}
Debug.Assert(redisValueAndExpiry.Expiry.HasValue, "Redis entry is expected to have an expiry.");
var storablePersistedGrant = MessagePackSerializer.Deserialize<StorablePersistedGrant>(redisValueAndExpiry.Value, _options);
return new PersistedGrant
{
Key = key,
Type = storablePersistedGrant.Type,
SubjectId = storablePersistedGrant.SubjectId,
SessionId = storablePersistedGrant.SessionId,
ClientId = storablePersistedGrant.ClientId,
Description = storablePersistedGrant.Description,
CreationTime = storablePersistedGrant.CreationTime,
ConsumedTime = storablePersistedGrant.ConsumedTime,
Data = storablePersistedGrant.Data,
Expiration = storablePersistedGrant.CreationTime.Add(redisValueAndExpiry.Expiry.Value),
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failure in 'GetAsync' using primary grant store, falling back.");
return await _fallbackGrantStore.GetAsync(key);
}
}
public Task RemoveAllAsync(PersistedGrantFilter filter)
{
_logger.LogWarning("This does not implement 'RemoveAllAsync', Skipping.");
return Task.CompletedTask;
}
// This method is not actually expected to get called and instead redis will just get rid of the expired items
public async Task RemoveAsync(string key)
{
if (!_connectionMultiplexer.IsConnecting)
{
_logger.LogWarning("Redis is not connected, using fallback store to execute 'RemoveAsync', with {Key}", key);
await _fallbackGrantStore.RemoveAsync(key);
}
var redisDb = _connectionMultiplexer.GetDatabase();
await redisDb.KeyDeleteAsync(CreateRedisKey(key));
}
public async Task StoreAsync(PersistedGrant grant)
{
try
{
if (!_connectionMultiplexer.IsConnected)
{
_logger.LogWarning("Redis is not connected, using fallback store to execute 'StoreAsync', with {Key}", grant.Key);
await _fallbackGrantStore.StoreAsync(grant);
}
if (!grant.Expiration.HasValue)
{
throw new ArgumentException("A PersistedGrant is always expected to include an expiration time.");
}
var redisDb = _connectionMultiplexer.GetDatabase();
var redisKey = CreateRedisKey(grant.Key);
var serializedGrant = MessagePackSerializer.Serialize(new StorablePersistedGrant
{
Type = grant.Type,
SubjectId = grant.SubjectId,
SessionId = grant.SessionId,
ClientId = grant.ClientId,
Description = grant.Description,
CreationTime = grant.CreationTime,
ConsumedTime = grant.ConsumedTime,
Data = grant.Data,
}, _options);
await redisDb.StringSetAsync(redisKey, serializedGrant, grant.Expiration.Value - grant.CreationTime);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failure in 'StoreAsync' using primary grant store, falling back.");
await _fallbackGrantStore.StoreAsync(grant);
}
}
private static RedisKey CreateRedisKey(string key)
{
return $"grant-{key}";
}
// This is a slimmer version of PersistedGrant that removes the Key since that will be used as the key in Redis
// it also strips out the ExpirationDate since we use that to store the TTL and read that out when we retrieve the
// object and can use that information to fill in the Expiration property on PersistedGrant
// TODO: .NET 8 Make all properties required
[MessagePackObject]
public class StorablePersistedGrant
{
[Key(0)]
public string Type { get; set; }
[Key(1)]
public string SubjectId { get; set; }
[Key(2)]
public string SessionId { get; set; }
[Key(3)]
public string ClientId { get; set; }
[Key(4)]
public string Description { get; set; }
[Key(5)]
public DateTime CreationTime { get; set; }
[Key(6)]
public DateTime? ConsumedTime { get; set; }
[Key(7)]
public string Data { get; set; }
}
}

View File

@ -7,7 +7,6 @@ using Bit.SharedWeb.Utilities;
using Duende.IdentityServer.ResponseHandling;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores;
using StackExchange.Redis;
namespace Bit.Identity.Utilities;
@ -54,56 +53,18 @@ public static class ServiceCollectionExtensions
if (CoreHelpers.SettingHasValue(globalSettings.IdentityServer.CosmosConnectionString))
{
services.AddSingleton<IPersistedGrantStore>(sp => BuildCosmosGrantStore(sp, globalSettings));
}
else if (CoreHelpers.SettingHasValue(globalSettings.IdentityServer.RedisConnectionString))
{
services.AddSingleton<IPersistedGrantStore>(sp => BuildRedisGrantStore(sp, globalSettings));
services.AddSingleton<IPersistedGrantStore>(sp =>
new PersistedGrantStore(sp.GetRequiredKeyedService<IGrantRepository>("cosmos"),
g => new Core.Auth.Models.Data.GrantItem(g)));
}
else
{
services.AddTransient<IPersistedGrantStore>(sp => BuildSqlGrantStore(sp));
services.AddTransient<IPersistedGrantStore>(sp =>
new PersistedGrantStore(sp.GetRequiredService<IGrantRepository>(),
g => new Core.Auth.Entities.Grant(g)));
}
services.AddTransient<ICorsPolicyService, CustomCorsPolicyService>();
return identityServerBuilder;
}
private static PersistedGrantStore BuildCosmosGrantStore(IServiceProvider sp, GlobalSettings globalSettings)
{
if (!CoreHelpers.SettingHasValue(globalSettings.IdentityServer.CosmosConnectionString))
{
throw new ArgumentException("No cosmos config string available.");
}
return new PersistedGrantStore(
// TODO: Perhaps we want to evaluate moving this repo to DI as a keyed service singleton in .NET 8
new Core.Auth.Repositories.Cosmos.GrantRepository(globalSettings),
g => new Core.Auth.Models.Data.GrantItem(g),
fallbackGrantStore: BuildRedisGrantStore(sp, globalSettings, true));
}
private static RedisPersistedGrantStore BuildRedisGrantStore(IServiceProvider sp,
GlobalSettings globalSettings, bool allowNull = false)
{
if (!CoreHelpers.SettingHasValue(globalSettings.IdentityServer.RedisConnectionString))
{
if (allowNull)
{
return null;
}
throw new ArgumentException("No redis config string available.");
}
return new RedisPersistedGrantStore(
// TODO: .NET 8 create a keyed service for this connection multiplexer and even PersistedGrantStore
ConnectionMultiplexer.Connect(globalSettings.IdentityServer.RedisConnectionString),
sp.GetRequiredService<ILogger<RedisPersistedGrantStore>>(),
fallbackGrantStore: BuildSqlGrantStore(sp));
}
private static PersistedGrantStore BuildSqlGrantStore(IServiceProvider sp)
{
return new PersistedGrantStore(sp.GetRequiredService<IGrantRepository>(),
g => new Core.Auth.Entities.Grant(g));
}
}

View File

@ -11,6 +11,7 @@ using Bit.Core.Auth.Identity;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Auth.LoginFeatures;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.Services.Implementations;
using Bit.Core.Auth.UserFeatures;
@ -120,6 +121,7 @@ public static class ServiceCollectionExtensions
{
services.AddSingleton<IEventRepository, TableStorageRepos.EventRepository>();
services.AddSingleton<IInstallationDeviceRepository, TableStorageRepos.InstallationDeviceRepository>();
services.AddKeyedSingleton<IGrantRepository, Core.Auth.Repositories.Cosmos.GrantRepository>("cosmos");
}
return provider;