#![cfg(feature = "tls-rustls")]
use redis::acl::Rule;
use redis::{Commands, RedisResult};
use tempfile::TempDir;
mod support;
use crate::support::*;
use redis_test::utils::{ClientCertPaths, build_client_cert_with_custom_cn};
struct CertAuthTestContext {
server_ctx: TestContext,
client_cert_paths: ClientCertPaths,
server_tls_paths: redis_test::utils::TlsFilePaths,
_tempdir: TempDir,
}
impl CertAuthTestContext {
fn setup_connection(&self) -> redis::Connection {
self.server_ctx.connection()
}
fn cert_auth_connection(&self) -> redis::Connection {
use support::build_single_client_with_separate_client_cert;
let client = build_single_client_with_separate_client_cert(
self.server_ctx.server.connection_info(),
&self.server_tls_paths,
&self.client_cert_paths,
)
.expect("Failed to create client with client certificate");
client
.get_connection()
.expect("Failed to get connection with client certificate")
}
}
fn create_cert_auth_context_with_username(username: &str) -> CertAuthTestContext {
use redis_test::utils::build_keys_and_certs_for_tls;
use tempfile::TempDir;
let tempdir = TempDir::new().expect("Failed to create temp dir");
let tls_paths = build_keys_and_certs_for_tls(&tempdir);
let ca_key_path = tempdir.path().join("ca.key");
let client_cert_paths =
build_client_cert_with_custom_cn(&tempdir, username, &tls_paths.ca_crt, &ca_key_path);
let server_ctx = TestContext::new_with_cert_auth(tls_paths.clone());
CertAuthTestContext {
server_ctx,
client_cert_paths,
server_tls_paths: tls_paths,
_tempdir: tempdir,
}
}
fn generate_random_username() -> String {
use rand::RngExt;
format!("testcertuser{}", rand::rng().random_range(10000..99999))
}
#[test]
fn test_tls_certificate_authentication_with_matching_acl_user() {
run_test_if_version_supported!([REDIS_CE_8_6, VALKEY_9_0]);
let test_username = generate_random_username();
let ctx = create_cert_auth_context_with_username(&test_username);
let mut setup_conn = ctx.setup_connection();
let result: RedisResult<String> = setup_conn.acl_setuser_rules(
&test_username,
&[
Rule::On,
Rule::NoPass, Rule::AllKeys, Rule::AddCommand("GET".to_string()),
Rule::AddCommand("SET".to_string()),
Rule::AddCommand("PING".to_string()),
Rule::AddCommand("ACL|WHOAMI".to_string()),
],
);
assert!(result.is_ok(), "Failed to create ACL user: {result:?}");
let users: Vec<String> = setup_conn.acl_users().expect("Failed to get ACL users");
assert!(
users.contains(&test_username),
"User {test_username} not found in ACL users list"
);
drop(setup_conn);
let mut cert_conn = ctx.cert_auth_connection();
let whoami: String = cert_conn
.acl_whoami()
.expect("Failed to execute ACL WHOAMI");
assert_eq!(
whoami, test_username,
"Expected to be authenticated as '{test_username}', but got '{whoami}'"
);
const TEST_KEY: &str = "test_cert_auth_key";
const TEST_VALUE: &str = "test_value";
let _: () = cert_conn
.set(TEST_KEY, TEST_VALUE)
.expect("SET command should be allowed");
let value: String = cert_conn
.get(TEST_KEY)
.expect("GET command should be allowed");
assert_eq!(value, TEST_VALUE);
let del_result: RedisResult<()> = cert_conn.del(TEST_KEY);
assert!(
del_result.is_err(),
"DEL command should have failed (user doesn't have +del permission)"
);
}
#[test]
fn test_tls_certificate_authentication_no_matching_user() {
run_test_if_version_supported!([REDIS_CE_8_6, VALKEY_9_0]);
let test_username = generate_random_username();
let ctx = create_cert_auth_context_with_username(&test_username);
let mut setup_conn = ctx.setup_connection();
let users: Vec<String> = setup_conn.acl_users().expect("Failed to get ACL users");
assert!(
!users.contains(&test_username),
"User {test_username} should not exist for this test"
);
drop(setup_conn);
let mut cert_conn = ctx.cert_auth_connection();
let whoami: String = cert_conn
.acl_whoami()
.expect("Failed to execute ACL WHOAMI");
assert_eq!(
whoami, "default",
"Expected to fall back to 'default' user, but got '{whoami}'"
);
const TEST_KEY: &str = "test_cert_auth_fallback";
const TEST_VALUE: &str = "fallback_value";
let _: () = cert_conn
.set(TEST_KEY, TEST_VALUE)
.expect("SET should work as default user");
let value: String = cert_conn
.get(TEST_KEY)
.expect("GET should work as default user");
assert_eq!(value, TEST_VALUE);
}