too much for one commit
This commit is contained in:
178
tests/EntKube.Secrets.Tests/Domain/ServiceTokenTests.cs
Normal file
178
tests/EntKube.Secrets.Tests/Domain/ServiceTokenTests.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user