forked from gongxuegit/tiku-backend.net
feat(security): add distributed authorization foundation
This commit is contained in:
81
Tiku.IntegrationTests/RedisSecurityStoreTests.cs
Normal file
81
Tiku.IntegrationTests/RedisSecurityStoreTests.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StackExchange.Redis;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Tiku.Application.Security;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ServiceProvider BuildProvider(string connectionString, string environment)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddRedisSecurity(connectionString, environment);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user