108 lines
5.1 KiB
C#
108 lines
5.1 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StackExchange.Redis;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.Tenancy;
|
|
using Tiku.Infrastructure;
|
|
using Tiku.Infrastructure.Security;
|
|
|
|
namespace Tiku.IntegrationTests;
|
|
|
|
public sealed class RedisSecurityStoreTests
|
|
{
|
|
[Fact]
|
|
public async Task Unavailable_redis_fails_closed_for_security_operations()
|
|
{
|
|
await using var provider = BuildProvider(
|
|
"localhost:6399,connectTimeout=200,syncTimeout=200,asyncTimeout=200,abortConnect=false",
|
|
$"integration-unavailable-{Guid.NewGuid():N}");
|
|
var store = provider.GetRequiredService<IRedisSecurityStore>();
|
|
|
|
await Assert.ThrowsAsync<RedisSecurityUnavailableException>(() => store.ConsumeAsync(
|
|
[
|
|
new DistributedRateLimitBucket("password:ip:test", 1, TimeSpan.FromSeconds(1))
|
|
]));
|
|
Assert.False(await store.PingAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Two_instances_share_atomic_limit_and_keys_contain_no_plaintext_identifier()
|
|
{
|
|
var connectionString = Environment.GetEnvironmentVariable("TIKU_TEST_REDIS");
|
|
if (string.IsNullOrWhiteSpace(connectionString)) return;
|
|
|
|
var environment = $"integration-{Guid.NewGuid():N}";
|
|
await using var first = BuildProvider(connectionString, environment);
|
|
await using var second = BuildProvider(connectionString, environment);
|
|
var store1 = first.GetRequiredService<IRedisSecurityStore>();
|
|
var store2 = second.GetRequiredService<IRedisSecurityStore>();
|
|
const string phone = "13812345678";
|
|
var phoneHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(phone))).ToLowerInvariant();
|
|
var bucket = new DistributedRateLimitBucket(
|
|
$"sms-verify:tenant-id:login:{phoneHash}", 1, TimeSpan.FromMinutes(1));
|
|
|
|
try
|
|
{
|
|
var attempts = await Task.WhenAll(Enumerable.Range(0, 12).Select(index =>
|
|
(index & 1) == 0
|
|
? store1.ConsumeAsync([bucket])
|
|
: store2.ConsumeAsync([bucket])));
|
|
|
|
Assert.Single(attempts, result => result.Allowed);
|
|
Assert.All(attempts.Where(result => !result.Allowed), result => Assert.NotNull(result.RetryAfter));
|
|
var multiplexer = first.GetRequiredService<IConnectionMultiplexer>();
|
|
var server = multiplexer.GetServer(multiplexer.GetEndPoints().Single());
|
|
var keys = server.Keys(pattern: $"tiku:{environment}:*").Select(key => key.ToString()).ToArray();
|
|
Assert.NotEmpty(keys);
|
|
Assert.DoesNotContain(keys, key => key.Contains(phone, StringComparison.Ordinal));
|
|
}
|
|
finally
|
|
{
|
|
var multiplexer = first.GetRequiredService<IConnectionMultiplexer>();
|
|
var server = multiplexer.GetServer(multiplexer.GetEndPoints().Single());
|
|
var keys = server.Keys(pattern: $"tiku:{environment}:*").ToArray();
|
|
if (keys.Length > 0) await multiplexer.GetDatabase().KeyDeleteAsync(keys);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Authorization_state_is_shared_and_older_version_cannot_overwrite_newer_version()
|
|
{
|
|
var connectionString = Environment.GetEnvironmentVariable("TIKU_TEST_REDIS");
|
|
if (string.IsNullOrWhiteSpace(connectionString)) return;
|
|
|
|
var environment = $"integration-authz-{Guid.NewGuid():N}";
|
|
await using var first = BuildProvider(connectionString, environment);
|
|
await using var second = BuildProvider(connectionString, environment);
|
|
var writer = first.GetRequiredService<IAccessSecurityCache>();
|
|
var reader = second.GetRequiredService<IAccessSecurityCache>();
|
|
var tenantId = Guid.NewGuid();
|
|
var userId = Guid.NewGuid();
|
|
var sessionId = Guid.NewGuid();
|
|
var lookup = new AccessSecurityCacheLookup(sessionId, userId, AuthRealm.Tenant, tenantId);
|
|
await writer.SetAsync(new AccessSecurityCacheState(
|
|
new CachedSessionSecurityState(sessionId, userId, AuthRealm.Tenant, tenantId, "stamp",
|
|
DateTimeOffset.UtcNow.AddMinutes(15), false),
|
|
new CachedUserSecurityState(userId, UserStatus.Active, "stamp"),
|
|
new CachedTenantSecurityState(tenantId, TenantStatus.Active),
|
|
new CachedMembershipSecurityState(tenantId, userId, MembershipStatus.Active),
|
|
null,
|
|
new CachedAuthorizationVersion(AuthRealm.Tenant, tenantId, 1)));
|
|
|
|
Assert.True((await reader.GetAsync(lookup))?.Complete);
|
|
await writer.SetAuthorizationVersionAsync(AuthRealm.Tenant, tenantId, 5);
|
|
await writer.SetAuthorizationVersionAsync(AuthRealm.Tenant, tenantId, 4);
|
|
Assert.Equal(5, (await reader.GetAsync(lookup))?.AuthorizationVersion?.Version);
|
|
}
|
|
|
|
private static ServiceProvider BuildProvider(string connectionString, string environment)
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.Configure<AuthorizationCacheOptions>(_ => { });
|
|
services.AddRedisSecurity(connectionString, environment);
|
|
return services.BuildServiceProvider();
|
|
}
|
|
} |