using EntKube.Secrets.Crypto; using EntKube.Secrets.Domain; using EntKube.Secrets.Infrastructure; using FluentAssertions; namespace EntKube.Secrets.Tests.Domain; /// /// 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. /// 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 { 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 { 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 { 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 { 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 { 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 { new("a/", AccessOperation.Read) }); await vault.CreateServiceTokenAsync(tenantId, "service-b", new List { new("b/", AccessOperation.Read | AccessOperation.Write) }); List 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 { new("shared/", AccessOperation.Read) }); TokenValidationResult result = await pod2.ValidateTokenAsync(token.Token); result.IsValid.Should().BeTrue(); result.Name.Should().Be("cross-pod-service"); } }