179 lines
6.1 KiB
C#
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");
|
|
}
|
|
}
|