Files
Entkube/tests/EntKube.Secrets.Tests/Domain/ServiceTokenTests.cs
2026-05-13 14:01:32 +02:00

179 lines
6.1 KiB
C#

using EntKube.Secrets.Crypto;
using EntKube.Secrets.Domain;
using EntKube.Secrets.Infrastructure;
using FluentAssertions;
namespace EntKube.Secrets.Tests.Domain;
/// <summary>
/// Tests for service token authentication. Other EntKube services authenticate
/// to the secrets service using service tokens scoped to specific path prefixes
/// with specific permissions. Tokens are now tenant-scoped.
/// </summary>
public class ServiceTokenTests
{
private readonly InMemoryVaultRepository repository = new();
private readonly byte[] kek = AesGcmEncryptor.GenerateKey();
private readonly Guid tenantId = Guid.NewGuid();
private Vault CreateVault() => new(repository);
[Fact]
public async Task CreateToken_ReturnsTokenWithCorrectPolicies()
{
Vault vault = CreateVault();
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
ServiceTokenResult token = await vault.CreateServiceTokenAsync(
tenantId,
name: "clusters-service",
policies: new List<AccessPolicy>
{
new("clusters/", AccessOperation.Read | AccessOperation.Write | AccessOperation.List),
new("shared/certificates/", AccessOperation.Read)
});
token.Token.Should().NotBeNullOrEmpty();
token.Name.Should().Be("clusters-service");
token.Policies.Should().HaveCount(2);
}
[Fact]
public async Task ValidateToken_WithValidToken_ReturnsTrue()
{
Vault vault = CreateVault();
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
ServiceTokenResult token = await vault.CreateServiceTokenAsync(
tenantId,
name: "test-service",
policies: new List<AccessPolicy>
{
new("test/", AccessOperation.Read)
});
TokenValidationResult result = await vault.ValidateTokenAsync(token.Token);
result.IsValid.Should().BeTrue();
result.Name.Should().Be("test-service");
}
[Fact]
public async Task ValidateToken_WithInvalidToken_ReturnsFalse()
{
Vault vault = CreateVault();
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
TokenValidationResult result = await vault.ValidateTokenAsync("invalid-token-that-was-never-issued");
result.IsValid.Should().BeFalse();
}
[Fact]
public async Task CheckAccess_WithMatchingPolicy_AllowsOperation()
{
Vault vault = CreateVault();
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
ServiceTokenResult token = await vault.CreateServiceTokenAsync(
tenantId,
name: "clusters-service",
policies: new List<AccessPolicy>
{
new("clusters/", AccessOperation.Read | AccessOperation.Write)
});
bool canRead = await vault.CheckAccessAsync(token.Token, "clusters/prod/dns-cred", AccessOperation.Read);
bool canWrite = await vault.CheckAccessAsync(token.Token, "clusters/prod/dns-cred", AccessOperation.Write);
canRead.Should().BeTrue();
canWrite.Should().BeTrue();
}
[Fact]
public async Task CheckAccess_WithNoMatchingPolicy_DeniesOperation()
{
Vault vault = CreateVault();
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
ServiceTokenResult token = await vault.CreateServiceTokenAsync(
tenantId,
name: "readonly-service",
policies: new List<AccessPolicy>
{
new("clusters/", AccessOperation.Read)
});
bool canWrite = await vault.CheckAccessAsync(token.Token, "clusters/prod/secret", AccessOperation.Write);
bool canReadOther = await vault.CheckAccessAsync(token.Token, "identity/users/", AccessOperation.Read);
canWrite.Should().BeFalse();
canReadOther.Should().BeFalse();
}
[Fact]
public async Task RevokeToken_PreventsSubsequentValidation()
{
Vault vault = CreateVault();
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
ServiceTokenResult token = await vault.CreateServiceTokenAsync(
tenantId,
name: "temporary-service",
policies: new List<AccessPolicy>
{
new("temp/", AccessOperation.Read)
});
TokenValidationResult validBefore = await vault.ValidateTokenAsync(token.Token);
validBefore.IsValid.Should().BeTrue();
await vault.RevokeTokenAsync(token.Token);
TokenValidationResult validAfter = await vault.ValidateTokenAsync(token.Token);
validAfter.IsValid.Should().BeFalse();
}
[Fact]
public async Task ListTokens_ReturnsActiveTokenMetadata()
{
Vault vault = CreateVault();
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
await vault.CreateServiceTokenAsync(tenantId, "service-a", new List<AccessPolicy>
{
new("a/", AccessOperation.Read)
});
await vault.CreateServiceTokenAsync(tenantId, "service-b", new List<AccessPolicy>
{
new("b/", AccessOperation.Read | AccessOperation.Write)
});
List<ServiceTokenInfo> tokens = await vault.ListTokensAsync(tenantId);
tokens.Should().HaveCount(2);
tokens.Should().Contain(t => t.Name == "service-a");
tokens.Should().Contain(t => t.Name == "service-b");
}
[Fact]
public async Task MultiInstance_TokenCreatedOnOnePodValidatesOnAnother()
{
Vault pod1 = CreateVault();
await pod1.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
Vault pod2 = CreateVault();
await pod2.AutoUnsealAsync(kek);
ServiceTokenResult token = await pod1.CreateServiceTokenAsync(
tenantId,
name: "cross-pod-service",
policies: new List<AccessPolicy> { new("shared/", AccessOperation.Read) });
TokenValidationResult result = await pod2.ValidateTokenAsync(token.Token);
result.IsValid.Should().BeTrue();
result.Name.Should().Be("cross-pod-service");
}
}