mirror of
https://github.com/hanzokv/go.git
synced 2026-09-24 00:31:51 +00:00
274 lines
9.0 KiB
Go
274 lines
9.0 KiB
Go
package redis_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/kv-go/v9"
|
|
)
|
|
|
|
func init() {
|
|
// Initialize RedisVersion from environment variable for regular Go tests
|
|
// (Ginkgo tests initialize this in BeforeSuite)
|
|
if version := os.Getenv("REDIS_VERSION"); version != "" {
|
|
if v, err := strconv.ParseFloat(strings.Trim(version, "\""), 64); err == nil && v > 0 {
|
|
RedisVersion = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// skipBeforeRedisVersion checks if Redis version is below the specified version and skips the test if so
|
|
func skipBeforeRedisVersion(t *testing.T, version float64, msg string) {
|
|
t.Helper()
|
|
if RedisVersion < version {
|
|
t.Skipf("Skipping test: Redis version %.1f < %.1f: %s", RedisVersion, version, msg)
|
|
}
|
|
}
|
|
|
|
// TestTLSCertificateAuthentication tests that Redis automatically authenticates
|
|
// a user based on the CN field in the client's TLS certificate.
|
|
//
|
|
// This test requires:
|
|
// 1. Redis 8.6+ configured with: tls-auth-clients-user CN
|
|
// 2. A client certificate with CN matching the Redis ACL username
|
|
// 3. The Docker image generates testcertuser.{crt,key} when TLS_CLIENT_CNS=testcertuser
|
|
//
|
|
// The test flow:
|
|
// 1. Create a Redis ACL user with a specific username (testcertuser)
|
|
// 2. Load the pre-generated client certificate with that username in the CN field
|
|
// 3. Connect using TLS with that certificate
|
|
// 4. Verify that Redis automatically authenticates as that user (no AUTH command needed)
|
|
func TestTLSCertificateAuthentication(t *testing.T) {
|
|
skipBeforeRedisVersion(t, 8.6, "tls-auth-clients-user CN requires Redis 8.6+")
|
|
|
|
ctx := context.Background()
|
|
testUsername := "testcertuser"
|
|
tlsCertDir := "dockers/standalone/tls"
|
|
|
|
// Step 1: Create a non-TLS client to set up the ACL user
|
|
setupClient := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379", // Non-TLS port
|
|
})
|
|
defer setupClient.Close()
|
|
|
|
// Verify connection
|
|
if err := setupClient.Ping(ctx).Err(); err != nil {
|
|
t.Fatalf("Redis not available: %v", err)
|
|
}
|
|
|
|
// Clean up any existing test user
|
|
setupClient.ACLDelUser(ctx, testUsername)
|
|
|
|
// Step 2: Create ACL user with specific permissions
|
|
// The user can read/write keys but has limited command access
|
|
err := setupClient.ACLSetUser(ctx,
|
|
testUsername,
|
|
"on", // Enable the user
|
|
"nopass", // No password required (will use cert auth)
|
|
"~*", // Can access all keys
|
|
"+get", // Allow GET command
|
|
"+set", // Allow SET command
|
|
"+ping", // Allow PING command
|
|
"+acl|whoami", // Allow ACL WHOAMI command
|
|
).Err()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create ACL user: %v", err)
|
|
}
|
|
defer setupClient.ACLDelUser(ctx, testUsername) // Cleanup
|
|
|
|
// Verify user was created
|
|
users, err := setupClient.ACLUsers(ctx).Result()
|
|
if err != nil {
|
|
t.Fatalf("Failed to list ACL users: %v", err)
|
|
}
|
|
t.Logf("ACL users: %v", users)
|
|
|
|
// Step 3: Load CA certificate for server verification
|
|
caCertPEM, err := os.ReadFile(tlsCertDir + "/ca.crt")
|
|
if err != nil {
|
|
t.Fatalf("CA cert not found: %v", err)
|
|
}
|
|
|
|
// Step 4: Load the pre-generated client certificate with CN=testcertuser
|
|
// This certificate is generated by the Docker image when TLS_CLIENT_CNS=testcertuser
|
|
clientCert, err := tls.LoadX509KeyPair(
|
|
tlsCertDir+"/"+testUsername+".crt",
|
|
tlsCertDir+"/"+testUsername+".key",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Client certificate not found: %v (ensure TLS_CLIENT_CNS=%s is set)", err, testUsername)
|
|
}
|
|
|
|
// Step 5: Create TLS config with the client certificate
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCertPEM)
|
|
|
|
tlsConfig := &tls.Config{
|
|
RootCAs: caCertPool,
|
|
Certificates: []tls.Certificate{clientCert},
|
|
ServerName: "localhost",
|
|
InsecureSkipVerify: true, // Using self-signed certs
|
|
}
|
|
|
|
// Step 6: Connect with TLS using the certificate
|
|
// NOTE: This test requires Redis to be configured with:
|
|
// tls-auth-clients-user CN
|
|
// Without this config, the certificate CN won't be used for authentication
|
|
tlsClient := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6666", // TLS port
|
|
TLSConfig: tlsConfig,
|
|
// NO Username/Password - authentication should happen via certificate!
|
|
})
|
|
defer tlsClient.Close()
|
|
|
|
// Step 7: Verify we're authenticated as the correct user
|
|
whoami, err := tlsClient.ACLWhoAmI(ctx).Result()
|
|
if err != nil {
|
|
t.Fatalf("ACL WHOAMI failed: %v (this test requires Redis to be configured with: tls-auth-clients-user CN)", err)
|
|
}
|
|
|
|
if whoami != testUsername {
|
|
t.Fatalf("Expected to be authenticated as %q, but got %q. Ensure Redis is configured with: tls-auth-clients-user CN", testUsername, whoami)
|
|
}
|
|
t.Logf("✅ Successfully authenticated as %q using certificate CN", whoami)
|
|
|
|
// Step 8: Test that we can execute allowed commands
|
|
err = tlsClient.Set(ctx, "test_cert_auth_key", "test_value", 0).Err()
|
|
if err != nil {
|
|
t.Fatalf("SET command failed (should be allowed): %v", err)
|
|
}
|
|
|
|
val, err := tlsClient.Get(ctx, "test_cert_auth_key").Result()
|
|
if err != nil {
|
|
t.Fatalf("GET command failed (should be allowed): %v", err)
|
|
}
|
|
if val != "test_value" {
|
|
t.Errorf("Expected 'test_value', got %q", val)
|
|
}
|
|
|
|
// Step 9: Test that we CANNOT execute disallowed commands
|
|
// The user doesn't have +del permission, so this should fail
|
|
err = tlsClient.Del(ctx, "test_cert_auth_key").Err()
|
|
if err == nil {
|
|
t.Error("DEL command succeeded but should have failed (user doesn't have +del permission)")
|
|
} else {
|
|
t.Logf("✅ DEL command correctly denied: %v", err)
|
|
}
|
|
|
|
// Cleanup
|
|
setupClient.Del(ctx, "test_cert_auth_key")
|
|
|
|
t.Log("✅ TLS certificate authentication test passed")
|
|
}
|
|
|
|
// TestTLSCertificateAuthenticationNoUser tests that when a certificate CN
|
|
// doesn't match any existing ACL user, Redis falls back to the default user.
|
|
//
|
|
// This test:
|
|
// 1. Ensures the testcertuser ACL user does NOT exist
|
|
// 2. Connects with a certificate that has CN=testcertuser
|
|
// 3. Verifies that Redis authenticates as "default" (fallback behavior)
|
|
func TestTLSCertificateAuthenticationNoUser(t *testing.T) {
|
|
skipBeforeRedisVersion(t, 8.6, "tls-auth-clients-user CN requires Redis 8.6+")
|
|
|
|
ctx := context.Background()
|
|
testUsername := "testcertuser"
|
|
tlsCertDir := "dockers/standalone/tls"
|
|
|
|
// Step 1: Create a non-TLS client to ensure the user does NOT exist
|
|
setupClient := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379", // Non-TLS port
|
|
})
|
|
defer setupClient.Close()
|
|
|
|
// Verify connection
|
|
if err := setupClient.Ping(ctx).Err(); err != nil {
|
|
t.Fatalf("Redis not available: %v", err)
|
|
}
|
|
|
|
// Delete the test user if it exists - we want to test fallback behavior
|
|
setupClient.ACLDelUser(ctx, testUsername)
|
|
|
|
// Verify user does not exist
|
|
users, err := setupClient.ACLUsers(ctx).Result()
|
|
if err != nil {
|
|
t.Fatalf("Failed to list ACL users: %v", err)
|
|
}
|
|
for _, u := range users {
|
|
if u == testUsername {
|
|
t.Fatalf("User %q should not exist for this test", testUsername)
|
|
}
|
|
}
|
|
t.Logf("ACL users (should not contain %s): %v", testUsername, users)
|
|
|
|
// Step 2: Load CA certificate for server verification
|
|
caCertPEM, err := os.ReadFile(tlsCertDir + "/ca.crt")
|
|
if err != nil {
|
|
t.Fatalf("CA cert not found: %v", err)
|
|
}
|
|
|
|
// Step 3: Load the client certificate with CN=testcertuser
|
|
// Even though the user doesn't exist, we still use this certificate
|
|
clientCert, err := tls.LoadX509KeyPair(
|
|
tlsCertDir+"/"+testUsername+".crt",
|
|
tlsCertDir+"/"+testUsername+".key",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Client certificate not found: %v (ensure TLS_CLIENT_CNS=%s is set)", err, testUsername)
|
|
}
|
|
|
|
// Step 4: Create TLS config with the client certificate
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCertPEM)
|
|
|
|
tlsConfig := &tls.Config{
|
|
RootCAs: caCertPool,
|
|
Certificates: []tls.Certificate{clientCert},
|
|
ServerName: "localhost",
|
|
InsecureSkipVerify: true, // Using self-signed certs
|
|
}
|
|
|
|
// Step 5: Connect with TLS using the certificate
|
|
tlsClient := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6666", // TLS port
|
|
TLSConfig: tlsConfig,
|
|
})
|
|
defer tlsClient.Close()
|
|
|
|
// Step 6: Verify we're authenticated as "default" (fallback)
|
|
whoami, err := tlsClient.ACLWhoAmI(ctx).Result()
|
|
if err != nil {
|
|
t.Fatalf("ACL WHOAMI failed: %v (Redis may not be configured for certificate-based authentication)", err)
|
|
}
|
|
|
|
// When the CN user doesn't exist, Redis should fall back to "default"
|
|
if whoami != "default" {
|
|
t.Fatalf("Expected to be authenticated as %q (fallback), but got %q", "default", whoami)
|
|
}
|
|
t.Logf("✅ Correctly fell back to %q user (CN user %q does not exist)", whoami, testUsername)
|
|
|
|
// Step 7: Verify we can execute commands as default user
|
|
err = tlsClient.Set(ctx, "test_cert_auth_fallback_key", "fallback_value", 0).Err()
|
|
if err != nil {
|
|
t.Fatalf("SET command failed: %v", err)
|
|
}
|
|
|
|
val, err := tlsClient.Get(ctx, "test_cert_auth_fallback_key").Result()
|
|
if err != nil {
|
|
t.Fatalf("GET command failed: %v", err)
|
|
}
|
|
if val != "fallback_value" {
|
|
t.Errorf("Expected 'fallback_value', got %q", val)
|
|
}
|
|
|
|
// Cleanup
|
|
tlsClient.Del(ctx, "test_cert_auth_fallback_key")
|
|
|
|
t.Log("✅ TLS certificate authentication fallback test passed")
|
|
}
|